import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
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;
* @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);
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+package org.apache.poi.poifs.crypt;\r
+\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.security.GeneralSecurityException;\r
+\r
+import javax.crypto.Cipher;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.util.Internal;\r
+import org.apache.poi.util.LittleEndianInput;\r
+import org.apache.poi.util.LittleEndianInputStream;\r
+\r
+@Internal\r
+public abstract class ChunkedCipherInputStream extends LittleEndianInputStream {\r
+ private final int chunkSize;\r
+ private final int chunkMask;\r
+ private final int chunkBits;\r
+ \r
+ private int _lastIndex = 0;\r
+ private long _pos = 0;\r
+ private long _size;\r
+ private byte[] _chunk;\r
+ private Cipher _cipher;\r
+\r
+ public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize)\r
+ throws GeneralSecurityException {\r
+ super((InputStream)stream);\r
+ _size = size;\r
+ this.chunkSize = chunkSize;\r
+ chunkMask = chunkSize-1;\r
+ chunkBits = Integer.bitCount(chunkMask);\r
+ \r
+ _cipher = initCipherForBlock(null, 0);\r
+ }\r
+ \r
+ protected abstract Cipher initCipherForBlock(Cipher existing, int block)\r
+ throws GeneralSecurityException;\r
+\r
+ public int read() throws IOException {\r
+ byte[] b = new byte[1];\r
+ if (read(b) == 1)\r
+ return b[0];\r
+ return -1;\r
+ }\r
+\r
+ // do not implement! -> recursion\r
+ // public int read(byte[] b) throws IOException;\r
+\r
+ public int read(byte[] b, int off, int len) throws IOException {\r
+ int total = 0;\r
+ \r
+ if (available() <= 0) return -1;\r
+\r
+ while (len > 0) {\r
+ if (_chunk == null) {\r
+ try {\r
+ _chunk = nextChunk();\r
+ } catch (GeneralSecurityException e) {\r
+ throw new EncryptedDocumentException(e.getMessage(), e);\r
+ }\r
+ }\r
+ int count = (int)(chunkSize - (_pos & chunkMask));\r
+ int avail = available();\r
+ if (avail == 0) {\r
+ return total;\r
+ }\r
+ count = Math.min(avail, Math.min(count, len));\r
+ System.arraycopy(_chunk, (int)(_pos & chunkMask), b, off, count);\r
+ off += count;\r
+ len -= count;\r
+ _pos += count;\r
+ if ((_pos & chunkMask) == 0)\r
+ _chunk = null;\r
+ total += count;\r
+ }\r
+\r
+ return total;\r
+ }\r
+\r
+ @Override\r
+ public long skip(long n) throws IOException {\r
+ long start = _pos;\r
+ long skip = Math.min(available(), n);\r
+\r
+ if ((((_pos + skip) ^ start) & ~chunkMask) != 0)\r
+ _chunk = null;\r
+ _pos += skip;\r
+ return skip;\r
+ }\r
+\r
+ @Override\r
+ public int available() {\r
+ return (int)(_size - _pos);\r
+ }\r
+ \r
+ @Override\r
+ public boolean markSupported() {\r
+ return false;\r
+ }\r
+\r
+ @Override\r
+ public synchronized void mark(int readlimit) {\r
+ throw new UnsupportedOperationException();\r
+ }\r
+ \r
+ @Override\r
+ public synchronized void reset() throws IOException {\r
+ throw new UnsupportedOperationException();\r
+ }\r
+\r
+ private byte[] nextChunk() throws GeneralSecurityException, IOException {\r
+ int index = (int)(_pos >> chunkBits);\r
+ initCipherForBlock(_cipher, index);\r
+ \r
+ if (_lastIndex != index) {\r
+ super.skip((index - _lastIndex) << chunkBits);\r
+ }\r
+\r
+ byte[] block = new byte[Math.min(super.available(), chunkSize)];\r
+ super.read(block, 0, block.length);\r
+ _lastIndex = index + 1;\r
+ return _cipher.doFinal(block);\r
+ }\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+package org.apache.poi.poifs.crypt;\r
+\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.io.FileOutputStream;\r
+import java.io.FilterOutputStream;\r
+import java.io.IOException;\r
+import java.io.OutputStream;\r
+import java.security.GeneralSecurityException;\r
+\r
+import javax.crypto.Cipher;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.poifs.filesystem.POIFSWriterEvent;\r
+import org.apache.poi.poifs.filesystem.POIFSWriterListener;\r
+import org.apache.poi.util.Internal;\r
+import org.apache.poi.util.LittleEndian;\r
+import org.apache.poi.util.LittleEndianConsts;\r
+import org.apache.poi.util.TempFile;\r
+\r
+@Internal\r
+public abstract class ChunkedCipherOutputStream extends FilterOutputStream {\r
+ protected final int chunkSize;\r
+ protected final int chunkMask;\r
+ protected final int chunkBits;\r
+ \r
+ private final byte[] _chunk;\r
+ private final File fileOut;\r
+ private final DirectoryNode dir;\r
+\r
+ private long _pos = 0;\r
+ private Cipher _cipher;\r
+ \r
+ public ChunkedCipherOutputStream(DirectoryNode dir, int chunkSize) throws IOException, GeneralSecurityException {\r
+ super(null);\r
+ this.chunkSize = chunkSize;\r
+ chunkMask = chunkSize-1;\r
+ chunkBits = Integer.bitCount(chunkMask);\r
+ _chunk = new byte[chunkSize];\r
+\r
+ fileOut = TempFile.createTempFile("encrypted_package", "crypt");\r
+ fileOut.deleteOnExit();\r
+ this.out = new FileOutputStream(fileOut);\r
+ this.dir = dir;\r
+ _cipher = initCipherForBlock(null, 0, false);\r
+ }\r
+\r
+ protected abstract Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk)\r
+ throws GeneralSecurityException; \r
+ \r
+ protected abstract void calculateChecksum(File fileOut, int oleStreamSize)\r
+ throws GeneralSecurityException, IOException;\r
+ \r
+ protected abstract void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)\r
+ throws IOException, GeneralSecurityException;\r
+\r
+ public void write(int b) throws IOException {\r
+ write(new byte[]{(byte)b});\r
+ }\r
+\r
+ public void write(byte[] b) throws IOException {\r
+ write(b, 0, b.length);\r
+ }\r
+\r
+ public void write(byte[] b, int off, int len)\r
+ throws IOException {\r
+ if (len == 0) return;\r
+ \r
+ if (len < 0 || b.length < off+len) {\r
+ throw new IOException("not enough bytes in your input buffer");\r
+ }\r
+ \r
+ while (len > 0) {\r
+ int posInChunk = (int)(_pos & chunkMask);\r
+ int nextLen = Math.min(chunkSize-posInChunk, len);\r
+ System.arraycopy(b, off, _chunk, posInChunk, nextLen);\r
+ _pos += nextLen;\r
+ off += nextLen;\r
+ len -= nextLen;\r
+ if ((_pos & chunkMask) == 0) {\r
+ try {\r
+ writeChunk();\r
+ } catch (GeneralSecurityException e) {\r
+ throw new IOException(e);\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ protected void writeChunk() throws IOException, GeneralSecurityException {\r
+ int posInChunk = (int)(_pos & chunkMask);\r
+ // normally posInChunk is 0, i.e. on the next chunk (-> index-1)\r
+ // but if called on close(), posInChunk is somewhere within the chunk data\r
+ int index = (int)(_pos >> chunkBits);\r
+ boolean lastChunk;\r
+ if (posInChunk==0) {\r
+ index--;\r
+ posInChunk = chunkSize;\r
+ lastChunk = false;\r
+ } else {\r
+ // pad the last chunk\r
+ lastChunk = true;\r
+ }\r
+\r
+ _cipher = initCipherForBlock(_cipher, index, lastChunk);\r
+\r
+ int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);\r
+ out.write(_chunk, 0, ciLen);\r
+ }\r
+ \r
+ public void close() throws IOException {\r
+ try {\r
+ writeChunk();\r
+\r
+ super.close();\r
+ \r
+ int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);\r
+ calculateChecksum(fileOut, oleStreamSize);\r
+ dir.createDocument("EncryptedPackage", oleStreamSize, new EncryptedPackageWriter());\r
+ createEncryptionInfoEntry(dir, fileOut);\r
+ } catch (GeneralSecurityException e) {\r
+ throw new IOException(e);\r
+ }\r
+ }\r
+\r
+ private class EncryptedPackageWriter implements POIFSWriterListener {\r
+ public void processPOIFSWriterEvent(POIFSWriterEvent event) {\r
+ try {\r
+ OutputStream os = event.getStream();\r
+ byte buf[] = new byte[chunkSize];\r
+ \r
+ // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data \r
+ // encrypted within the EncryptedData field, not including the size of the StreamSize field. \r
+ // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this \r
+ // value, depending on the block size of the chosen encryption algorithm\r
+ LittleEndian.putLong(buf, 0, _pos);\r
+ os.write(buf, 0, LittleEndian.LONG_SIZE);\r
+\r
+ FileInputStream fis = new FileInputStream(fileOut);\r
+ int readBytes;\r
+ while ((readBytes = fis.read(buf)) != -1) {\r
+ os.write(buf, 0, readBytes);\r
+ }\r
+ fis.close();\r
+\r
+ os.close();\r
+ \r
+ fileOut.delete();\r
+ } catch (IOException e) {\r
+ throw new EncryptedDocumentException(e);\r
+ }\r
+ }\r
+ }\r
+}\r
import org.apache.poi.EncryptedDocumentException;\r
\r
public enum CipherProvider {\r
- rc4("RC4", 1),\r
- aes("AES", 0x18);\r
+ rc4("RC4", 1, "Microsoft Base Cryptographic Provider v1.0"),\r
+ aes("AES", 0x18, "Microsoft Enhanced RSA and AES Cryptographic Provider");\r
\r
public static CipherProvider fromEcmaId(int ecmaId) {\r
for (CipherProvider cp : CipherProvider.values()) {\r
\r
public final String jceId;\r
public final int ecmaId;\r
- CipherProvider(String jceId, int ecmaId) {\r
+ public final String cipherProviderName;\r
+ CipherProvider(String jceId, int ecmaId, String cipherProviderName) {\r
this.jceId = jceId;\r
this.ecmaId = ecmaId;\r
+ this.cipherProviderName = cipherProviderName;\r
}\r
}
\ No newline at end of file
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;
}
/**
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
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
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;
/**
*/
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());
}
}
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;
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
, 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
, 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
, 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;
\r
import java.io.IOException;\r
\r
-import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.LittleEndianInput;\r
\r
public interface EncryptionInfoBuilder {\r
- void initialize(EncryptionInfo ei, DocumentInputStream dis) throws IOException;\r
+ /**\r
+ * initialize the builder from a stream\r
+ */\r
+ void initialize(EncryptionInfo ei, LittleEndianInput dis) throws IOException;\r
+\r
+ /**\r
+ * initialize the builder from scratch\r
+ */\r
void initialize(EncryptionInfo ei, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode);\r
+\r
+ /**\r
+ * @return the header data\r
+ */\r
EncryptionHeader getHeader();\r
+\r
+ /**\r
+ * @return the verifier data\r
+ */\r
EncryptionVerifier getVerifier();\r
+\r
+ /**\r
+ * @return the decryptor\r
+ */\r
Decryptor getDecryptor();\r
+\r
+ /**\r
+ * @return the encryptor\r
+ */\r
Encryptor getEncryptor();\r
}\r
\r
package org.apache.poi.poifs.crypt;\r
\r
+import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;\r
+\r
+/**\r
+ * Office supports various encryption modes.\r
+ * The encryption is either based on the whole container ({@link #agile}, {@link #standard} or {@link #binaryRC4})\r
+ * or record based ({@link #cryptoAPI}). The record based encryption can't be accessed directly, but will be\r
+ * invoked by using the {@link Biff8EncryptionKey#setCurrentUserPassword(String)} before saving the document.\r
+ */\r
public enum EncryptionMode {\r
- standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24)\r
- , agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40);\r
+ /* @see <a href="http://msdn.microsoft.com/en-us/library/dd907466(v=office.12).aspx">2.3.6 Office Binary Document RC4 Encryption</a> */\r
+ binaryRC4("org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionInfoBuilder", 1, 1, 0x0),\r
+ /* @see <a href="http://msdn.microsoft.com/en-us/library/dd905225(v=office.12).aspx">2.3.5 Office Binary Document RC4 CryptoAPI Encryption</a> */\r
+ cryptoAPI("org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionInfoBuilder", 4, 2, 0x04),\r
+ /* @see <a href="http://msdn.microsoft.com/en-us/library/dd906097(v=office.12).aspx">2.3.4.5 \EncryptionInfo Stream (Standard Encryption)</a> */\r
+ standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24),\r
+ /* @see <a href="http://msdn.microsoft.com/en-us/library/dd925810(v=office.12).aspx">2.3.4.10 \EncryptionInfo Stream (Agile Encryption)</a> */\r
+ agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40)\r
+ ;\r
\r
public final String builder;\r
public final int versionMajor;\r
* The method name is misleading - you'll get the encrypted verifier, not the plain verifier
* @deprecated use getEncryptedVerifier()
*/
+ @Deprecated
public byte[] getVerifier() {
return encryptedVerifier;
}
* 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;
}
/**
* @deprecated use getCipherAlgorithm().jceId
*/
+ @Deprecated
public String getAlgorithmName() {
return cipherAlgorithm.jceId;
}
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.binaryrc4;\r
+\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.security.GeneralSecurityException;\r
+import java.security.MessageDigest;\r
+import java.util.Arrays;\r
+import javax.crypto.Cipher;\r
+import javax.crypto.SecretKey;\r
+import javax.crypto.spec.SecretKeySpec;\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.*;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.LittleEndian;\r
+\r
+public class BinaryRC4Decryptor extends Decryptor {\r
+ private long _length = -1L;\r
+ \r
+ private class BinaryRC4CipherInputStream extends ChunkedCipherInputStream {\r
+\r
+ protected Cipher initCipherForBlock(Cipher existing, int block)\r
+ throws GeneralSecurityException {\r
+ return BinaryRC4Decryptor.initCipherForBlock(existing, block, builder, getSecretKey(), Cipher.DECRYPT_MODE);\r
+ }\r
+\r
+ public BinaryRC4CipherInputStream(DocumentInputStream stream, long size)\r
+ throws GeneralSecurityException {\r
+ super(stream, size, 512);\r
+ }\r
+ }\r
+\r
+ protected BinaryRC4Decryptor(BinaryRC4EncryptionInfoBuilder builder) {\r
+ super(builder);\r
+ }\r
+\r
+ public boolean verifyPassword(String password) {\r
+ EncryptionVerifier ver = builder.getVerifier();\r
+ SecretKey skey = generateSecretKey(password, ver);\r
+ try {\r
+ Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE);\r
+ byte encryptedVerifier[] = ver.getEncryptedVerifier();\r
+ byte verifier[] = new byte[encryptedVerifier.length];\r
+ cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier);\r
+ setVerifier(verifier);\r
+ byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();\r
+ byte verifierHash[] = cipher.doFinal(encryptedVerifierHash);\r
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+ byte calcVerifierHash[] = hashAlg.digest(verifier);\r
+ if (Arrays.equals(calcVerifierHash, verifierHash)) {\r
+ setSecretKey(skey);\r
+ return true;\r
+ }\r
+ } catch (GeneralSecurityException e) {\r
+ throw new EncryptedDocumentException(e);\r
+ }\r
+ return false;\r
+ }\r
+\r
+ protected static Cipher initCipherForBlock(Cipher cipher, int block,\r
+ EncryptionInfoBuilder builder, SecretKey skey, int encryptMode)\r
+ throws GeneralSecurityException {\r
+ EncryptionVerifier ver = builder.getVerifier();\r
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+ byte blockKey[] = new byte[4];\r
+ LittleEndian.putUInt(blockKey, 0, block);\r
+ byte encKey[] = CryptoFunctions.generateKey(skey.getEncoded(), hashAlgo, blockKey, 16);\r
+ SecretKey key = new SecretKeySpec(encKey, skey.getAlgorithm());\r
+ if (cipher == null) {\r
+ EncryptionHeader em = builder.getHeader();\r
+ cipher = CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), null, null, encryptMode);\r
+ } else {\r
+ cipher.init(encryptMode, key);\r
+ }\r
+ return cipher;\r
+ }\r
+\r
+ protected static SecretKey generateSecretKey(String password,\r
+ EncryptionVerifier ver) {\r
+ if (password.length() > 255)\r
+ password = password.substring(0, 255);\r
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+ byte hash[] = hashAlg.digest(CryptoFunctions.getUtf16LeString(password));\r
+ byte salt[] = ver.getSalt();\r
+ hashAlg.reset();\r
+ for (int i = 0; i < 16; i++) {\r
+ hashAlg.update(hash, 0, 5);\r
+ hashAlg.update(salt);\r
+ }\r
+\r
+ hash = new byte[5];\r
+ System.arraycopy(hashAlg.digest(), 0, hash, 0, 5);\r
+ SecretKey skey = new SecretKeySpec(hash, ver.getCipherAlgorithm().jceId);\r
+ return skey;\r
+ }\r
+\r
+ public InputStream getDataStream(DirectoryNode dir) throws IOException,\r
+ GeneralSecurityException {\r
+ DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");\r
+ _length = dis.readLong();\r
+ BinaryRC4CipherInputStream cipherStream = new BinaryRC4CipherInputStream(dis, _length);\r
+ return cipherStream;\r
+ }\r
+\r
+ public long getLength() {\r
+ if (_length == -1L) {\r
+ throw new IllegalStateException("Decryptor.getDataStream() was not called");\r
+ }\r
+ \r
+ return _length;\r
+ }\r
+}
\ No newline at end of file
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.binaryrc4;\r
+\r
+import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
+import org.apache.poi.poifs.crypt.CipherProvider;\r
+import org.apache.poi.poifs.crypt.EncryptionHeader;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
+\r
+public class BinaryRC4EncryptionHeader extends EncryptionHeader implements\r
+ EncryptionRecord {\r
+\r
+ protected BinaryRC4EncryptionHeader() {\r
+ setCipherAlgorithm(CipherAlgorithm.rc4);\r
+ setKeySize(40);\r
+ setBlockSize(-1);\r
+ setCipherProvider(CipherProvider.rc4);\r
+ setHashAlgorithm(HashAlgorithm.md5);\r
+ setSizeExtra(0);\r
+ setFlags(0);\r
+ setCspName("");\r
+ setChainingMode(null);\r
+ }\r
+\r
+ public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) {\r
+ }\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.binaryrc4;\r
+\r
+import java.io.IOException;\r
+import org.apache.poi.poifs.crypt.*;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class BinaryRC4EncryptionInfoBuilder implements EncryptionInfoBuilder {\r
+\r
+ EncryptionInfo info;\r
+ BinaryRC4EncryptionHeader header;\r
+ BinaryRC4EncryptionVerifier verifier;\r
+ BinaryRC4Decryptor decryptor;\r
+ BinaryRC4Encryptor encryptor;\r
+\r
+ public BinaryRC4EncryptionInfoBuilder() {\r
+ }\r
+\r
+ public void initialize(EncryptionInfo info, LittleEndianInput dis)\r
+ throws IOException {\r
+ this.info = info;\r
+ int vMajor = info.getVersionMajor();\r
+ int vMinor = info.getVersionMinor();\r
+ assert (vMajor == 1 && vMinor == 1);\r
+\r
+ header = new BinaryRC4EncryptionHeader();\r
+ verifier = new BinaryRC4EncryptionVerifier(dis);\r
+ decryptor = new BinaryRC4Decryptor(this);\r
+ encryptor = new BinaryRC4Encryptor(this);\r
+ }\r
+\r
+ public void initialize(EncryptionInfo info,\r
+ CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm,\r
+ int keyBits, int blockSize, ChainingMode chainingMode) {\r
+ this.info = info;\r
+ header = new BinaryRC4EncryptionHeader();\r
+ verifier = new BinaryRC4EncryptionVerifier();\r
+ decryptor = new BinaryRC4Decryptor(this);\r
+ encryptor = new BinaryRC4Encryptor(this);\r
+ }\r
+\r
+ public BinaryRC4EncryptionHeader getHeader() {\r
+ return header;\r
+ }\r
+\r
+ public BinaryRC4EncryptionVerifier getVerifier() {\r
+ return verifier;\r
+ }\r
+\r
+ public BinaryRC4Decryptor getDecryptor() {\r
+ return decryptor;\r
+ }\r
+\r
+ public BinaryRC4Encryptor getEncryptor() {\r
+ return encryptor;\r
+ }\r
+\r
+ public EncryptionInfo getEncryptionInfo() {\r
+ return info;\r
+ }\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.binaryrc4;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.*;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class BinaryRC4EncryptionVerifier extends EncryptionVerifier implements EncryptionRecord {\r
+\r
+ protected BinaryRC4EncryptionVerifier() {\r
+ setSpinCount(-1);\r
+ setCipherAlgorithm(CipherAlgorithm.rc4);\r
+ setChainingMode(null);\r
+ setEncryptedKey(null);\r
+ setHashAlgorithm(HashAlgorithm.md5);\r
+ }\r
+\r
+ protected BinaryRC4EncryptionVerifier(LittleEndianInput is) {\r
+ byte salt[] = new byte[16];\r
+ is.readFully(salt);\r
+ setSalt(salt);\r
+ byte encryptedVerifier[] = new byte[16];\r
+ is.readFully(encryptedVerifier);\r
+ setEncryptedVerifier(encryptedVerifier);\r
+ byte encryptedVerifierHash[] = new byte[16];\r
+ is.readFully(encryptedVerifierHash);\r
+ setEncryptedVerifierHash(encryptedVerifierHash);\r
+ setSpinCount(-1);\r
+ setCipherAlgorithm(CipherAlgorithm.rc4);\r
+ setChainingMode(null);\r
+ setEncryptedKey(null);\r
+ setHashAlgorithm(HashAlgorithm.md5);\r
+ }\r
+\r
+ protected void setSalt(byte salt[]) {\r
+ if (salt == null || salt.length != 16) {\r
+ throw new EncryptedDocumentException("invalid verifier salt");\r
+ }\r
+ \r
+ super.setSalt(salt);\r
+ }\r
+\r
+ protected void setEncryptedVerifier(byte encryptedVerifier[]) {\r
+ super.setEncryptedVerifier(encryptedVerifier);\r
+ }\r
+\r
+ protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) {\r
+ super.setEncryptedVerifierHash(encryptedVerifierHash);\r
+ }\r
+\r
+ public void write(LittleEndianByteArrayOutputStream bos) {\r
+ byte salt[] = getSalt();\r
+ assert (salt.length == 16);\r
+ bos.write(salt);\r
+ byte encryptedVerifier[] = getEncryptedVerifier();\r
+ assert (encryptedVerifier.length == 16);\r
+ bos.write(encryptedVerifier);\r
+ byte encryptedVerifierHash[] = getEncryptedVerifierHash();\r
+ assert (encryptedVerifierHash.length == 16);\r
+ bos.write(encryptedVerifierHash);\r
+ }\r
+\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.binaryrc4;\r
+\r
+import java.io.File;\r
+import java.io.IOException;\r
+import java.io.OutputStream;\r
+import java.security.GeneralSecurityException;\r
+import java.security.MessageDigest;\r
+import java.security.SecureRandom;\r
+import java.util.Random;\r
+\r
+import javax.crypto.Cipher;\r
+import javax.crypto.SecretKey;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.DataSpaceMapUtils;\r
+import org.apache.poi.poifs.crypt.EncryptionInfo;\r
+import org.apache.poi.poifs.crypt.Encryptor;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
+\r
+public class BinaryRC4Encryptor extends Encryptor {\r
+\r
+ private final BinaryRC4EncryptionInfoBuilder builder;\r
+ \r
+ protected class BinaryRC4CipherOutputStream extends ChunkedCipherOutputStream {\r
+\r
+ protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk)\r
+ throws GeneralSecurityException {\r
+ return BinaryRC4Decryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE);\r
+ }\r
+\r
+ protected void calculateChecksum(File file, int i) {\r
+ }\r
+\r
+ protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)\r
+ throws IOException, GeneralSecurityException {\r
+ BinaryRC4Encryptor.this.createEncryptionInfoEntry(dir);\r
+ }\r
+\r
+ public BinaryRC4CipherOutputStream(DirectoryNode dir)\r
+ throws IOException, GeneralSecurityException {\r
+ super(dir, 512);\r
+ }\r
+ }\r
+\r
+ protected BinaryRC4Encryptor(BinaryRC4EncryptionInfoBuilder builder) {\r
+ this.builder = builder;\r
+ }\r
+\r
+ public void confirmPassword(String password) {\r
+ Random r = new SecureRandom();\r
+ byte salt[] = new byte[16];\r
+ byte verifier[] = new byte[16];\r
+ r.nextBytes(salt);\r
+ r.nextBytes(verifier);\r
+ confirmPassword(password, null, null, verifier, salt, null);\r
+ }\r
+\r
+ public void confirmPassword(String password, byte keySpec[],\r
+ byte keySalt[], byte verifier[], byte verifierSalt[],\r
+ byte integritySalt[]) {\r
+ BinaryRC4EncryptionVerifier ver = builder.getVerifier();\r
+ ver.setSalt(verifierSalt);\r
+ SecretKey skey = BinaryRC4Decryptor.generateSecretKey(password, ver);\r
+ setSecretKey(skey);\r
+ try {\r
+ Cipher cipher = BinaryRC4Decryptor.initCipherForBlock(null, 0, builder, skey, Cipher.ENCRYPT_MODE);\r
+ byte encryptedVerifier[] = new byte[16];\r
+ cipher.update(verifier, 0, 16, encryptedVerifier);\r
+ ver.setEncryptedVerifier(encryptedVerifier);\r
+ org.apache.poi.poifs.crypt.HashAlgorithm hashAlgo = ver\r
+ .getHashAlgorithm();\r
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+ byte calcVerifierHash[] = hashAlg.digest(verifier);\r
+ byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash);\r
+ ver.setEncryptedVerifierHash(encryptedVerifierHash);\r
+ } catch (GeneralSecurityException e) {\r
+ throw new EncryptedDocumentException("Password confirmation failed", e);\r
+ }\r
+ }\r
+\r
+ public OutputStream getDataStream(DirectoryNode dir)\r
+ throws IOException, GeneralSecurityException {\r
+ OutputStream countStream = new BinaryRC4CipherOutputStream(dir);\r
+ return countStream;\r
+ }\r
+\r
+ protected int getKeySizeInBytes() {\r
+ return builder.getHeader().getKeySize() / 8;\r
+ }\r
+\r
+ protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {\r
+ DataSpaceMapUtils.addDefaultDataSpace(dir);\r
+ final EncryptionInfo info = builder.getEncryptionInfo();\r
+ final BinaryRC4EncryptionHeader header = builder.getHeader();\r
+ final BinaryRC4EncryptionVerifier verifier = builder.getVerifier();\r
+ EncryptionRecord er = new EncryptionRecord() {\r
+ public void write(LittleEndianByteArrayOutputStream bos) {\r
+ bos.writeShort(info.getVersionMajor());\r
+ bos.writeShort(info.getVersionMinor());\r
+ header.write(bos);\r
+ verifier.write(bos);\r
+ }\r
+ };\r
+ DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er);\r
+ }\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.cryptoapi;\r
+\r
+import java.io.ByteArrayInputStream;\r
+import java.io.ByteArrayOutputStream;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.nio.charset.Charset;\r
+import java.security.GeneralSecurityException;\r
+import java.security.MessageDigest;\r
+import java.util.Arrays;\r
+\r
+import javax.crypto.Cipher;\r
+import javax.crypto.SecretKey;\r
+import javax.crypto.ShortBufferException;\r
+import javax.crypto.spec.SecretKeySpec;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.Decryptor;\r
+import org.apache.poi.poifs.crypt.EncryptionHeader;\r
+import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;\r
+import org.apache.poi.poifs.crypt.EncryptionVerifier;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.poifs.filesystem.DocumentNode;\r
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;\r
+import org.apache.poi.util.BitField;\r
+import org.apache.poi.util.BitFieldFactory;\r
+import org.apache.poi.util.BoundedInputStream;\r
+import org.apache.poi.util.IOUtils;\r
+import org.apache.poi.util.LittleEndian;\r
+import org.apache.poi.util.LittleEndianInputStream;\r
+\r
+public class CryptoAPIDecryptor extends Decryptor {\r
+\r
+ private long _length;\r
+ \r
+ private class SeekableByteArrayInputStream extends ByteArrayInputStream {\r
+ Cipher cipher;\r
+ byte oneByte[] = { 0 };\r
+ \r
+ public void seek(int pos) {\r
+ if (pos > count) {\r
+ throw new ArrayIndexOutOfBoundsException(pos);\r
+ }\r
+ \r
+ this.pos = pos;\r
+ mark = pos;\r
+ }\r
+\r
+ public void setBlock(int block) throws GeneralSecurityException {\r
+ cipher = initCipherForBlock(cipher, block);\r
+ }\r
+\r
+ public synchronized int read() {\r
+ int ch = super.read();\r
+ if (ch == -1) return -1;\r
+ oneByte[0] = (byte) ch;\r
+ try {\r
+ cipher.update(oneByte, 0, 1, oneByte);\r
+ } catch (ShortBufferException e) {\r
+ throw new EncryptedDocumentException(e);\r
+ }\r
+ return oneByte[0];\r
+ }\r
+\r
+ public synchronized int read(byte b[], int off, int len) {\r
+ int readLen = super.read(b, off, len);\r
+ if (readLen ==-1) return -1;\r
+ try {\r
+ cipher.update(b, off, readLen, b, off);\r
+ } catch (ShortBufferException e) {\r
+ throw new EncryptedDocumentException(e);\r
+ }\r
+ return readLen;\r
+ }\r
+\r
+ public SeekableByteArrayInputStream(byte buf[])\r
+ throws GeneralSecurityException {\r
+ super(buf);\r
+ cipher = initCipherForBlock(null, 0);\r
+ }\r
+ }\r
+\r
+ static class StreamDescriptorEntry {\r
+ static BitField flagStream = BitFieldFactory.getInstance(1);\r
+ \r
+ int streamOffset;\r
+ int streamSize;\r
+ int block;\r
+ int flags;\r
+ int reserved2;\r
+ String streamName;\r
+ }\r
+\r
+ protected CryptoAPIDecryptor(CryptoAPIEncryptionInfoBuilder builder) {\r
+ super(builder);\r
+ _length = -1L;\r
+ }\r
+\r
+ public boolean verifyPassword(String password) {\r
+ EncryptionVerifier ver = builder.getVerifier();\r
+ SecretKey skey = generateSecretKey(password, ver);\r
+ try {\r
+ Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE);\r
+ byte encryptedVerifier[] = ver.getEncryptedVerifier();\r
+ byte verifier[] = new byte[encryptedVerifier.length];\r
+ cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier);\r
+ setVerifier(verifier);\r
+ byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();\r
+ byte verifierHash[] = cipher.doFinal(encryptedVerifierHash);\r
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+ byte calcVerifierHash[] = hashAlg.digest(verifier);\r
+ if (Arrays.equals(calcVerifierHash, verifierHash)) {\r
+ setSecretKey(skey);\r
+ return true;\r
+ }\r
+ } catch (GeneralSecurityException e) {\r
+ throw new EncryptedDocumentException(e);\r
+ }\r
+ return false;\r
+ }\r
+\r
+ /**\r
+ * Initializes a cipher object for a given block index for decryption\r
+ *\r
+ * @param cipher may be null, otherwise the given instance is reset to the new block index\r
+ * @param block the block index, e.g. the persist/slide id (hslf)\r
+ * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher\r
+ * @throws GeneralSecurityException\r
+ */\r
+ public Cipher initCipherForBlock(Cipher cipher, int block)\r
+ throws GeneralSecurityException {\r
+ return initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.DECRYPT_MODE);\r
+ }\r
+\r
+ protected static Cipher initCipherForBlock(Cipher cipher, int block,\r
+ EncryptionInfoBuilder builder, SecretKey skey, int encryptMode)\r
+ throws GeneralSecurityException {\r
+ EncryptionVerifier ver = builder.getVerifier();\r
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+ byte blockKey[] = new byte[4];\r
+ LittleEndian.putUInt(blockKey, 0, block);\r
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+ hashAlg.update(skey.getEncoded());\r
+ byte encKey[] = hashAlg.digest(blockKey);\r
+ EncryptionHeader header = builder.getHeader();\r
+ int keyBits = header.getKeySize();\r
+ encKey = CryptoFunctions.getBlock0(encKey, keyBits / 8);\r
+ if (keyBits == 40) {\r
+ encKey = CryptoFunctions.getBlock0(encKey, 16);\r
+ }\r
+ SecretKey key = new SecretKeySpec(encKey, skey.getAlgorithm());\r
+ if (cipher == null) {\r
+ cipher = CryptoFunctions.getCipher(key, header.getCipherAlgorithm(), null, null, encryptMode);\r
+ } else {\r
+ cipher.init(encryptMode, key);\r
+ }\r
+ return cipher;\r
+ }\r
+\r
+ protected static SecretKey generateSecretKey(String password, EncryptionVerifier ver) {\r
+ if (password.length() > 255) {\r
+ password = password.substring(0, 255);\r
+ }\r
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+ hashAlg.update(ver.getSalt());\r
+ byte hash[] = hashAlg.digest(CryptoFunctions.getUtf16LeString(password));\r
+ SecretKey skey = new SecretKeySpec(hash, ver.getCipherAlgorithm().jceId);\r
+ return skey;\r
+ }\r
+\r
+ /**\r
+ * Decrypt the Document-/SummaryInformation and other optionally streams.\r
+ * Opposed to other crypto modes, cryptoapi is record based and can't be used\r
+ * to stream-decrypt a whole file\r
+ * \r
+ * @see <a href="http://msdn.microsoft.com/en-us/library/dd943321(v=office.12).aspx">2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream</a>\r
+ */\r
+ @SuppressWarnings("unused")\r
+ public InputStream getDataStream(DirectoryNode dir)\r
+ throws IOException, GeneralSecurityException {\r
+ POIFSFileSystem fsOut = new POIFSFileSystem();\r
+ DocumentNode es = (DocumentNode) dir.getEntry("EncryptedSummary");\r
+ DocumentInputStream dis = dir.createDocumentInputStream(es);\r
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
+ IOUtils.copy(dis, bos);\r
+ dis.close();\r
+ SeekableByteArrayInputStream sbis = new SeekableByteArrayInputStream(bos.toByteArray());\r
+ LittleEndianInputStream leis = new LittleEndianInputStream(sbis);\r
+ int streamDescriptorArrayOffset = (int) leis.readUInt();\r
+ int streamDescriptorArraySize = (int) leis.readUInt();\r
+ sbis.skip(streamDescriptorArrayOffset - 8);\r
+ sbis.setBlock(0);\r
+ int encryptedStreamDescriptorCount = (int) leis.readUInt();\r
+ StreamDescriptorEntry entries[] = new StreamDescriptorEntry[encryptedStreamDescriptorCount];\r
+ for (int i = 0; i < encryptedStreamDescriptorCount; i++) {\r
+ StreamDescriptorEntry entry = new StreamDescriptorEntry();\r
+ entries[i] = entry;\r
+ entry.streamOffset = (int) leis.readUInt();\r
+ entry.streamSize = (int) leis.readUInt();\r
+ entry.block = leis.readUShort();\r
+ int nameSize = leis.readUByte();\r
+ entry.flags = leis.readUByte();\r
+ boolean isStream = StreamDescriptorEntry.flagStream.isSet(entry.flags);\r
+ entry.reserved2 = leis.readInt();\r
+ byte nameBuf[] = new byte[nameSize * 2];\r
+ leis.read(nameBuf);\r
+ entry.streamName = new String(nameBuf, Charset.forName("UTF-16LE"));\r
+ leis.readShort();\r
+ assert(entry.streamName.length() == nameSize);\r
+ }\r
+\r
+ for (StreamDescriptorEntry entry : entries) {\r
+ sbis.seek(entry.streamOffset);\r
+ sbis.setBlock(entry.block);\r
+ InputStream is = new BoundedInputStream(sbis, entry.streamSize);\r
+ fsOut.createDocument(is, entry.streamName);\r
+ }\r
+\r
+ leis.close();\r
+ sbis = null;\r
+ bos.reset();\r
+ fsOut.writeFilesystem(bos);\r
+ _length = bos.size();\r
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());\r
+ return bis;\r
+ }\r
+\r
+ /**\r
+ * @return the length of the stream returned by {@link #getDataStream(DirectoryNode)}\r
+ */\r
+ public long getLength() {\r
+ if (_length == -1L) {\r
+ throw new IllegalStateException("Decryptor.getDataStream() was not called");\r
+ }\r
+ return _length;\r
+ }\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.cryptoapi;\r
+\r
+import java.io.IOException;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.poifs.crypt.ChainingMode;\r
+import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
+import org.apache.poi.poifs.crypt.CipherProvider;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.standard.StandardEncryptionHeader;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class CryptoAPIEncryptionHeader extends StandardEncryptionHeader {\r
+\r
+ public CryptoAPIEncryptionHeader(LittleEndianInput is) throws IOException {\r
+ super(is);\r
+ }\r
+\r
+ protected CryptoAPIEncryptionHeader(CipherAlgorithm cipherAlgorithm,\r
+ HashAlgorithm hashAlgorithm, int keyBits, int blockSize,\r
+ ChainingMode chainingMode) {\r
+ super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
+ }\r
+\r
+ public void setKeySize(int keyBits) {\r
+ // Microsoft Base Cryptographic Provider is limited up to 40 bits\r
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375599(v=vs.85).aspx\r
+ boolean found = false;\r
+ for (int size : getCipherAlgorithm().allowedKeySize) {\r
+ if (size == keyBits) {\r
+ found = true;\r
+ break;\r
+ }\r
+ }\r
+ if (!found) {\r
+ throw new EncryptedDocumentException("invalid keysize "+keyBits+" for cipher algorithm "+getCipherAlgorithm());\r
+ }\r
+ super.setKeySize(keyBits);\r
+ if (keyBits > 40) {\r
+ setCspName("Microsoft Enhanced Cryptographic Provider v1.0");\r
+ } else {\r
+ setCspName(CipherProvider.rc4.cipherProviderName);\r
+ }\r
+ }\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.cryptoapi;\r
+\r
+import java.io.IOException;\r
+\r
+import org.apache.poi.poifs.crypt.*;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class CryptoAPIEncryptionInfoBuilder implements EncryptionInfoBuilder {\r
+ EncryptionInfo info;\r
+ CryptoAPIEncryptionHeader header;\r
+ CryptoAPIEncryptionVerifier verifier;\r
+ CryptoAPIDecryptor decryptor;\r
+ CryptoAPIEncryptor encryptor;\r
+\r
+ public CryptoAPIEncryptionInfoBuilder() {\r
+ }\r
+\r
+ /**\r
+ * initialize the builder from a stream\r
+ */\r
+ @SuppressWarnings("unused")\r
+ public void initialize(EncryptionInfo info, LittleEndianInput dis)\r
+ throws IOException {\r
+ this.info = info;\r
+ int hSize = dis.readInt();\r
+ header = new CryptoAPIEncryptionHeader(dis);\r
+ verifier = new CryptoAPIEncryptionVerifier(dis, header);\r
+ decryptor = new CryptoAPIDecryptor(this);\r
+ encryptor = new CryptoAPIEncryptor(this);\r
+ }\r
+\r
+ /**\r
+ * initialize the builder from scratch\r
+ */\r
+ public void initialize(EncryptionInfo info,\r
+ CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm,\r
+ int keyBits, int blockSize, ChainingMode chainingMode) {\r
+ this.info = info;\r
+ if (cipherAlgorithm == null) cipherAlgorithm = CipherAlgorithm.rc4;\r
+ if (hashAlgorithm == null) hashAlgorithm = HashAlgorithm.sha1;\r
+ if (keyBits == -1) keyBits = 0x28; \r
+ assert(cipherAlgorithm == CipherAlgorithm.rc4 && hashAlgorithm == HashAlgorithm.sha1);\r
+ \r
+ header = new CryptoAPIEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
+ verifier = new CryptoAPIEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
+ decryptor = new CryptoAPIDecryptor(this);\r
+ encryptor = new CryptoAPIEncryptor(this);\r
+ }\r
+\r
+ public CryptoAPIEncryptionHeader getHeader() {\r
+ return header;\r
+ }\r
+\r
+ public CryptoAPIEncryptionVerifier getVerifier() {\r
+ return verifier;\r
+ }\r
+\r
+ public CryptoAPIDecryptor getDecryptor() {\r
+ return decryptor;\r
+ }\r
+\r
+ public CryptoAPIEncryptor getEncryptor() {\r
+ return encryptor;\r
+ }\r
+\r
+ public EncryptionInfo getEncryptionInfo() {\r
+ return info;\r
+ }\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.cryptoapi;\r
+\r
+import org.apache.poi.poifs.crypt.ChainingMode;\r
+import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.standard.StandardEncryptionVerifier;\r
+import org.apache.poi.util.LittleEndianInput;\r
+\r
+public class CryptoAPIEncryptionVerifier extends StandardEncryptionVerifier {\r
+\r
+ protected CryptoAPIEncryptionVerifier(LittleEndianInput is,\r
+ CryptoAPIEncryptionHeader header) {\r
+ super(is, header);\r
+ }\r
+\r
+ protected CryptoAPIEncryptionVerifier(CipherAlgorithm cipherAlgorithm,\r
+ HashAlgorithm hashAlgorithm, int keyBits, int blockSize,\r
+ ChainingMode chainingMode) {\r
+ super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
+ }\r
+\r
+ protected void setSalt(byte salt[]) {\r
+ super.setSalt(salt);\r
+ }\r
+\r
+ protected void setEncryptedVerifier(byte encryptedVerifier[]) {\r
+ super.setEncryptedVerifier(encryptedVerifier);\r
+ }\r
+\r
+ protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) {\r
+ super.setEncryptedVerifierHash(encryptedVerifierHash);\r
+ }\r
+}\r
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.poifs.crypt.cryptoapi;\r
+\r
+import java.io.ByteArrayInputStream;\r
+import java.io.ByteArrayOutputStream;\r
+import java.io.IOException;\r
+import java.io.OutputStream;\r
+import java.nio.charset.Charset;\r
+import java.security.GeneralSecurityException;\r
+import java.security.MessageDigest;\r
+import java.security.SecureRandom;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+import java.util.Random;\r
+\r
+import javax.crypto.Cipher;\r
+import javax.crypto.SecretKey;\r
+\r
+import org.apache.poi.EncryptedDocumentException;\r
+import org.apache.poi.hpsf.DocumentSummaryInformation;\r
+import org.apache.poi.hpsf.PropertySetFactory;\r
+import org.apache.poi.hpsf.SummaryInformation;\r
+import org.apache.poi.hpsf.WritingNotSupportedException;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.DataSpaceMapUtils;\r
+import org.apache.poi.poifs.crypt.EncryptionInfo;\r
+import org.apache.poi.poifs.crypt.Encryptor;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor.StreamDescriptorEntry;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
+import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.IOUtils;\r
+import org.apache.poi.util.LittleEndian;\r
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
+\r
+public class CryptoAPIEncryptor extends Encryptor {\r
+ private final CryptoAPIEncryptionInfoBuilder builder;\r
+\r
+ protected CryptoAPIEncryptor(CryptoAPIEncryptionInfoBuilder builder) {\r
+ this.builder = builder;\r
+ }\r
+\r
+ public void confirmPassword(String password) {\r
+ Random r = new SecureRandom();\r
+ byte salt[] = new byte[16];\r
+ byte verifier[] = new byte[16];\r
+ r.nextBytes(salt);\r
+ r.nextBytes(verifier);\r
+ confirmPassword(password, null, null, verifier, salt, null);\r
+ }\r
+\r
+ public void confirmPassword(String password, byte keySpec[],\r
+ byte keySalt[], byte verifier[], byte verifierSalt[],\r
+ byte integritySalt[]) {\r
+ assert(verifier != null && verifierSalt != null);\r
+ CryptoAPIEncryptionVerifier ver = builder.getVerifier();\r
+ ver.setSalt(verifierSalt);\r
+ SecretKey skey = CryptoAPIDecryptor.generateSecretKey(password, ver);\r
+ setSecretKey(skey);\r
+ try {\r
+ Cipher cipher = initCipherForBlock(null, 0);\r
+ byte encryptedVerifier[] = new byte[verifier.length];\r
+ cipher.update(verifier, 0, verifier.length, encryptedVerifier);\r
+ ver.setEncryptedVerifier(encryptedVerifier);\r
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+ MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo);\r
+ byte calcVerifierHash[] = hashAlg.digest(verifier);\r
+ byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash);\r
+ ver.setEncryptedVerifierHash(encryptedVerifierHash);\r
+ } catch (GeneralSecurityException e) {\r
+ throw new EncryptedDocumentException("Password confirmation failed", e);\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Initializes a cipher object for a given block index for encryption\r
+ *\r
+ * @param cipher may be null, otherwise the given instance is reset to the new block index\r
+ * @param block the block index, e.g. the persist/slide id (hslf)\r
+ * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher\r
+ * @throws GeneralSecurityException\r
+ */\r
+ public Cipher initCipherForBlock(Cipher cipher, int block)\r
+ throws GeneralSecurityException {\r
+ return CryptoAPIDecryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE);\r
+ } \r
+ \r
+ /**\r
+ * Encrypt the Document-/SummaryInformation and other optionally streams.\r
+ * Opposed to other crypto modes, cryptoapi is record based and can't be used\r
+ * to stream-encrypt a whole file\r
+ * \r
+ * @see <a href="http://msdn.microsoft.com/en-us/library/dd943321(v=office.12).aspx">2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream</a>\r
+ */\r
+ public OutputStream getDataStream(DirectoryNode dir)\r
+ throws IOException, GeneralSecurityException {\r
+ CipherByteArrayOutputStream bos = new CipherByteArrayOutputStream();\r
+ byte buf[] = new byte[8];\r
+ \r
+ bos.write(buf, 0, 8); // skip header\r
+ String entryNames[] = {\r
+ SummaryInformation.DEFAULT_STREAM_NAME,\r
+ DocumentSummaryInformation.DEFAULT_STREAM_NAME\r
+ };\r
+ \r
+ List<StreamDescriptorEntry> descList = new ArrayList<StreamDescriptorEntry>();\r
+\r
+ int block = 0;\r
+ for (String entryName : entryNames) {\r
+ if (!dir.hasEntry(entryName)) continue;\r
+ StreamDescriptorEntry descEntry = new StreamDescriptorEntry();\r
+ descEntry.block = block;\r
+ descEntry.streamOffset = bos.size();\r
+ descEntry.streamName = entryName;\r
+ descEntry.flags = StreamDescriptorEntry.flagStream.setValue(0, 1);\r
+ descEntry.reserved2 = 0;\r
+ \r
+ bos.setBlock(block);\r
+ DocumentInputStream dis = dir.createDocumentInputStream(entryName);\r
+ IOUtils.copy(dis, bos);\r
+ dis.close();\r
+ \r
+ descEntry.streamSize = bos.size() - descEntry.streamOffset;\r
+ descList.add(descEntry);\r
+ \r
+ dir.getEntry(entryName).delete();\r
+ \r
+ block++;\r
+ }\r
+ \r
+ int streamDescriptorArrayOffset = bos.size();\r
+ \r
+ bos.setBlock(0);\r
+ LittleEndian.putUInt(buf, 0, descList.size());\r
+ bos.write(buf, 0, 4);\r
+ \r
+ for (StreamDescriptorEntry sde : descList) {\r
+ LittleEndian.putUInt(buf, 0, sde.streamOffset);\r
+ bos.write(buf, 0, 4);\r
+ LittleEndian.putUInt(buf, 0, sde.streamSize);\r
+ bos.write(buf, 0, 4);\r
+ LittleEndian.putUShort(buf, 0, sde.block);\r
+ bos.write(buf, 0, 2);\r
+ LittleEndian.putUByte(buf, 0, (short)sde.streamName.length());\r
+ bos.write(buf, 0, 1);\r
+ LittleEndian.putUByte(buf, 0, (short)sde.flags);\r
+ bos.write(buf, 0, 1);\r
+ LittleEndian.putUInt(buf, 0, sde.reserved2);\r
+ bos.write(buf, 0, 4);\r
+ byte nameBytes[] = sde.streamName.getBytes(Charset.forName("UTF-16LE"));\r
+ bos.write(nameBytes, 0, nameBytes.length);\r
+ LittleEndian.putShort(buf, 0, (short)0); // null-termination\r
+ bos.write(buf, 0, 2);\r
+ }\r
+ \r
+ int savedSize = bos.size();\r
+ int streamDescriptorArraySize = savedSize - streamDescriptorArrayOffset;\r
+ LittleEndian.putUInt(buf, 0, streamDescriptorArrayOffset);\r
+ LittleEndian.putUInt(buf, 4, streamDescriptorArraySize);\r
+\r
+ bos.reset();\r
+ bos.setBlock(0);\r
+ bos.write(buf, 0, 8);\r
+ bos.setSize(savedSize);\r
+ \r
+ dir.createDocument("EncryptedSummary", new ByteArrayInputStream(bos.getBuf(), 0, savedSize));\r
+ DocumentSummaryInformation dsi = PropertySetFactory.newDocumentSummaryInformation();\r
+ \r
+ try {\r
+ dsi.write(dir, DocumentSummaryInformation.DEFAULT_STREAM_NAME);\r
+ } catch (WritingNotSupportedException e) {\r
+ throw new IOException(e);\r
+ }\r
+ \r
+ return bos;\r
+ }\r
+\r
+ protected int getKeySizeInBytes() {\r
+ return builder.getHeader().getKeySize() / 8;\r
+ }\r
+\r
+ protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {\r
+ DataSpaceMapUtils.addDefaultDataSpace(dir);\r
+ final EncryptionInfo info = builder.getEncryptionInfo();\r
+ final CryptoAPIEncryptionHeader header = builder.getHeader();\r
+ final CryptoAPIEncryptionVerifier verifier = builder.getVerifier();\r
+ EncryptionRecord er = new EncryptionRecord() {\r
+ public void write(LittleEndianByteArrayOutputStream bos) {\r
+ bos.writeShort(info.getVersionMajor());\r
+ bos.writeShort(info.getVersionMinor());\r
+ header.write(bos);\r
+ verifier.write(bos);\r
+ }\r
+ };\r
+ DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er);\r
+ }\r
+\r
+ private class CipherByteArrayOutputStream extends ByteArrayOutputStream {\r
+ Cipher cipher;\r
+ byte oneByte[] = { 0 };\r
+\r
+ public CipherByteArrayOutputStream() throws GeneralSecurityException {\r
+ setBlock(0);\r
+ }\r
+ \r
+ public byte[] getBuf() {\r
+ return buf;\r
+ }\r
+ \r
+ public void setSize(int count) {\r
+ this.count = count;\r
+ }\r
+ \r
+ public void setBlock(int block) throws GeneralSecurityException {\r
+ cipher = initCipherForBlock(cipher, block);\r
+ }\r
+ \r
+ public void write(int b) {\r
+ try {\r
+ oneByte[0] = (byte)b;\r
+ cipher.update(oneByte, 0, 1, oneByte, 0);\r
+ super.write(oneByte);\r
+ } catch (Exception e) {\r
+ throw new EncryptedDocumentException(e);\r
+ }\r
+ }\r
+\r
+ public void write(byte[] b, int off, int len) {\r
+ try {\r
+ cipher.update(b, off, len, b, off);\r
+ super.write(b, off, len);\r
+ } catch (Exception e) {\r
+ throw new EncryptedDocumentException(e);\r
+ }\r
+ }\r
+\r
+ }\r
+}\r
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;
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);
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);
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;
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<length; i++) {
- result[i] = 0;
- }
- }
- return result;
- }
-
private Cipher getCipher(SecretKey key) {
- EncryptionHeader em = info.getHeader();
+ EncryptionHeader em = builder.getHeader();
ChainingMode cm = em.getChainingMode();
assert(cm == ChainingMode.ecb);
return CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), cm, null, Cipher.DECRYPT_MODE);
// limit wrong calculated ole entries - (bug #57080)
// standard encryption always uses aes encoding, so blockSize is always 16
// http://stackoverflow.com/questions/3283787/size-of-data-after-aes-encryption
- int blockSize = info.getHeader().getCipherAlgorithm().blockSize;
+ int blockSize = builder.getHeader().getCipherAlgorithm().blockSize;
long cipherLen = (_length/blockSize + 1) * blockSize;
Cipher cipher = getCipher(getSecretKey());
return new BoundedInputStream(new CipherInputStream(boundedDis, cipher), _length);
}
+ /**
+ * @return the length of the stream returned by {@link #getDataStream(DirectoryNode)}
+ */
public long getLength(){
if(_length == -1) throw new IllegalStateException("Decryptor.getDataStream() was not called");
return _length;
}
-
- protected int getKeySizeInBytes() {
- return info.getHeader().getKeySize()/8;
- }
}
package org.apache.poi.poifs.crypt.standard;\r
\r
import static org.apache.poi.poifs.crypt.CryptoFunctions.getUtf16LeString;\r
+import static org.apache.poi.poifs.crypt.EncryptionInfo.flagAES;\r
+import static org.apache.poi.poifs.crypt.EncryptionInfo.flagCryptoAPI;\r
\r
import java.io.IOException;\r
+import java.io.InputStream;\r
\r
import org.apache.poi.poifs.crypt.ChainingMode;\r
import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
import org.apache.poi.poifs.crypt.CipherProvider;\r
import org.apache.poi.poifs.crypt.EncryptionHeader;\r
import org.apache.poi.poifs.crypt.HashAlgorithm;\r
-import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
-import org.apache.poi.util.BitField;\r
import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
import org.apache.poi.util.LittleEndianConsts;\r
+import org.apache.poi.util.LittleEndianInput;\r
import org.apache.poi.util.LittleEndianOutput;\r
\r
public class StandardEncryptionHeader extends EncryptionHeader implements EncryptionRecord {\r
- // A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption \r
- // [ECMA-376] is used. It MUST be 1 unless fExternal is 1. If fExternal is 1, it MUST be 0.\r
- private static BitField flagsCryptoAPI = new BitField(0x04);\r
\r
- // A value that MUST be 0 if document properties are encrypted. The \r
- // encryption of document properties is specified in section 2.3.5.4 [MS-OFFCRYPTO].\r
- @SuppressWarnings("unused")\r
- private static BitField flagsDocProps = new BitField(0x08);\r
- \r
- // A value that MUST be 1 if extensible encryption is used,. If this value is 1, \r
- // the value of every other field in this structure MUST be 0.\r
- @SuppressWarnings("unused")\r
- private static BitField flagsExternal = new BitField(0x10);\r
- \r
- // A value that MUST be 1 if the protected content is an ECMA-376 document \r
- // [ECMA-376]. If the fAES bit is 1, the fCryptoAPI bit MUST also be 1.\r
- private static BitField flagsAES = new BitField(0x20);\r
- \r
- protected StandardEncryptionHeader(DocumentInputStream is) throws IOException {\r
+ protected StandardEncryptionHeader(LittleEndianInput is) throws IOException {\r
setFlags(is.readInt());\r
setSizeExtra(is.readInt());\r
setCipherAlgorithm(CipherAlgorithm.fromEcmaId(is.readInt()));\r
setHashAlgorithm(HashAlgorithm.fromEcmaId(is.readInt()));\r
- setKeySize(is.readInt());\r
+ int keySize = is.readInt();\r
+ if (keySize == 0) {\r
+ // for the sake of inheritance of the cryptoAPI classes\r
+ // see 2.3.5.1 RC4 CryptoAPI Encryption Header\r
+ // If set to 0x00000000, it MUST be interpreted as 0x00000028 bits.\r
+ keySize = 0x28;\r
+ }\r
+ setKeySize(keySize);\r
setBlockSize(getKeySize());\r
setCipherProvider(CipherProvider.fromEcmaId(is.readInt()));\r
\r
\r
// CSPName may not always be specified\r
// In some cases, the salt value of the EncryptionVerifier is the next chunk of data\r
- is.mark(LittleEndianConsts.INT_SIZE+1);\r
+ ((InputStream)is).mark(LittleEndianConsts.INT_SIZE+1);\r
int checkForSalt = is.readInt();\r
- is.reset();\r
+ ((InputStream)is).reset();\r
\r
if (checkForSalt == 16) {\r
setCspName("");\r
setKeySize(keyBits);\r
setBlockSize(blockSize);\r
setCipherProvider(cipherAlgorithm.provider);\r
- setFlags(flagsCryptoAPI.setBoolean(0, true)\r
- | flagsAES.setBoolean(0, cipherAlgorithm.provider == CipherProvider.aes));\r
+ setFlags(flagCryptoAPI.setBoolean(0, true)\r
+ | flagAES.setBoolean(0, cipherAlgorithm.provider == CipherProvider.aes));\r
// see http://msdn.microsoft.com/en-us/library/windows/desktop/bb931357(v=vs.85).aspx for a full list\r
// setCspName("Microsoft Enhanced RSA and AES Cryptographic Provider");\r
}\r
\r
+ /**\r
+ * serializes the header \r
+ */\r
public void write(LittleEndianByteArrayOutputStream bos) {\r
int startIdx = bos.getWriteIndex();\r
LittleEndianOutput sizeOutput = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE);\r
bos.writeInt(getCipherProvider().ecmaId);\r
bos.writeInt(0); // reserved1\r
bos.writeInt(0); // reserved2\r
- if (getCspName() != null) {\r
- bos.write(getUtf16LeString(getCspName()));\r
- bos.writeShort(0);\r
- }\r
+ String cspName = getCspName();\r
+ if (cspName == null) cspName = getCipherProvider().cipherProviderName;\r
+ bos.write(getUtf16LeString(cspName));\r
+ bos.writeShort(0);\r
int headerSize = bos.getWriteIndex()-startIdx-LittleEndianConsts.INT_SIZE;\r
sizeOutput.writeInt(headerSize); \r
}\r
import org.apache.poi.poifs.crypt.EncryptionInfo;\r
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;\r
import org.apache.poi.poifs.crypt.HashAlgorithm;\r
-import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.LittleEndianInput;\r
\r
public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {\r
\r
StandardDecryptor decryptor;\r
StandardEncryptor encryptor;\r
\r
- public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {\r
+ /**\r
+ * initialize the builder from a stream\r
+ */\r
+ public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException {\r
this.info = info;\r
\r
@SuppressWarnings("unused")\r
verifier = new StandardEncryptionVerifier(dis, header);\r
\r
if (info.getVersionMinor() == 2 && (info.getVersionMajor() == 3 || info.getVersionMajor() == 4)) {\r
- decryptor = new StandardDecryptor(info);\r
+ decryptor = new StandardDecryptor(this);\r
}\r
}\r
\r
+ /**\r
+ * initialize the builder from scratch\r
+ */\r
public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {\r
this.info = info;\r
\r
}\r
header = new StandardEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
verifier = new StandardEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);\r
- decryptor = new StandardDecryptor(info);\r
+ decryptor = new StandardDecryptor(this);\r
encryptor = new StandardEncryptor(this);\r
}\r
\r
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.HashAlgorithm;
-import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+import org.apache.poi.util.LittleEndianInput;
/**
* Used when checking if a key is valid for a document
private static final int SPIN_COUNT = 50000;
private final int verifierHashSize;
- protected StandardEncryptionVerifier(DocumentInputStream is, StandardEncryptionHeader header) {
+ protected StandardEncryptionVerifier(LittleEndianInput is, StandardEncryptionHeader header) {
int saltSize = is.readInt();
if (saltSize!=16) {
setEncryptedVerifierHash(encryptedVerifierHash);
setSpinCount(SPIN_COUNT);
- setCipherAlgorithm(CipherAlgorithm.aes128);
- setChainingMode(ChainingMode.ecb);
+ setCipherAlgorithm(header.getCipherAlgorithm());
+ setChainingMode(header.getChainingMode());
setEncryptedKey(null);
- setHashAlgorithm(HashAlgorithm.sha1);
+ setHashAlgorithm(header.getHashAlgorithmEx());
}
protected StandardEncryptionVerifier(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
assert(encryptedVerifier.length == 16);
bos.write(encryptedVerifier);
- // The number of bytes used by the encrypted Verifier hash MUST be 32.
// The number of bytes used by the decrypted Verifier hash is given by
// the VerifierHashSize field, which MUST be 20
- byte encryptedVerifierHash[] = getEncryptedVerifierHash();
- assert(encryptedVerifierHash.length == 32);
bos.writeInt(20);
+
+ // EncryptedVerifierHash: An array of bytes that contains the encrypted form of the hash of
+ // the randomly generated Verifier value. The length of the array MUST be the size of the
+ // encryption block size multiplied by the number of blocks needed to encrypt the hash of the
+ // Verifier. If the encryption algorithm is RC4, the length MUST be 20 bytes. If the encryption
+ // algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash
+ // field, only the first VerifierHashSize bytes MUST be used.
+ byte encryptedVerifierHash[] = getEncryptedVerifierHash();
+ assert(encryptedVerifierHash.length == getCipherAlgorithm().encryptedVerifierHashLength);
bos.write(encryptedVerifierHash);
}
\r
import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry;\r
import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.generateSecretKey;\r
-import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.truncateOrPad;\r
\r
import java.io.File;\r
import java.io.FileInputStream;\r
import java.security.GeneralSecurityException;\r
import java.security.MessageDigest;\r
import java.security.SecureRandom;\r
+import java.util.Arrays;\r
import java.util.Random;\r
\r
import javax.crypto.Cipher;\r
// algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash\r
// field, only the first VerifierHashSize bytes MUST be used.\r
int encVerHashSize = ver.getCipherAlgorithm().encryptedVerifierHashLength; \r
- byte encryptedVerifierHash[] = cipher.doFinal(truncateOrPad(calcVerifierHash, encVerHashSize));\r
+ byte encryptedVerifierHash[] = cipher.doFinal(Arrays.copyOf(calcVerifierHash, encVerHashSize));\r
\r
ver.setEncryptedVerifier(encryptedVerifier);\r
ver.setEncryptedVerifierHash(encryptedVerifierHash);\r
public int readUByte() {
return delegate.readUByte();
}
+
+ public long readUInt() {
+ int i = readInt();
+ return i & 0xFFFFFFFFL;
+ }
}
import java.io.IOException;
import java.io.InputStream;
+import org.apache.poi.util.LittleEndian.BufferUnderrunException;
+
/**
* Wraps an {@link InputStream} providing {@link LittleEndianInput}<p/>
*
public LittleEndianInputStream(InputStream is) {
super(is);
}
+
public int available() {
try {
return super.available();
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");
}
}
}
public void readFully(byte[] buf, int off, int len) {
- int max = off+len;
- for(int i=off; i<max; i++) {
- int ch;
- try {
- ch = in.read();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- checkEOF(ch);
- buf[i] = (byte) ch;
- }
+ try {
+ checkEOF(read(buf, off, len), len);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
}
import javax.crypto.spec.SecretKeySpec;
import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.poifs.crypt.ChunkedCipherInputStream;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.Decryptor;
import org.apache.poi.poifs.crypt.EncryptionHeader;
+import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
import org.apache.poi.poifs.crypt.EncryptionVerifier;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry;
* Decryptor implementation for Agile Encryption
*/
public class AgileDecryptor extends Decryptor {
- private final AgileEncryptionInfoBuilder builder;
-
-
private long _length = -1;
protected static final byte[] kVerifierInputBlock;
}
protected AgileDecryptor(AgileEncryptionInfoBuilder builder) {
- super(builder.getInfo());
- this.builder = builder;
+ super(builder);
}
/**
* set decryption password
*/
public boolean verifyPassword(String password) throws GeneralSecurityException {
- AgileEncryptionVerifier ver = builder.getVerifier();
- AgileEncryptionHeader header = builder.getHeader();
+ AgileEncryptionVerifier ver = (AgileEncryptionVerifier)builder.getVerifier();
+ AgileEncryptionHeader header = (AgileEncryptionHeader)builder.getHeader();
HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
int blockSize = header.getBlockSize();
* @throws GeneralSecurityException
*/
public boolean verifyPassword(KeyPair keyPair, X509Certificate x509) throws GeneralSecurityException {
- AgileEncryptionVerifier ver = builder.getVerifier();
- AgileEncryptionHeader header = builder.getHeader();
+ AgileEncryptionVerifier ver = (AgileEncryptionVerifier)builder.getVerifier();
+ AgileEncryptionHeader header = (AgileEncryptionHeader)builder.getHeader();
HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
int blockSize = header.getBlockSize();
return fillSize;
}
- protected static byte[] hashInput(AgileEncryptionInfoBuilder builder, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) {
+ protected static byte[] hashInput(EncryptionInfoBuilder builder, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) {
EncryptionVerifier ver = builder.getVerifier();
- int keySize = builder.getDecryptor().getKeySizeInBytes();
- int blockSize = builder.getDecryptor().getBlockSizeInBytes();
+ AgileDecryptor dec = (AgileDecryptor)builder.getDecryptor();
+ int keySize = dec.getKeySizeInBytes();
+ int blockSize = dec.getBlockSizeInBytes();
HashAlgorithm hashAlgo = ver.getHashAlgorithm();
byte[] salt = ver.getSalt();
DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
_length = dis.readLong();
- ChunkedCipherInputStream cipherStream = new ChunkedCipherInputStream(dis, _length);
+ ChunkedCipherInputStream cipherStream = new AgileCipherInputStream(dis, _length);
return cipherStream;
}
return _length;
}
+
+ protected static Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk, EncryptionInfoBuilder builder, SecretKey skey, int encryptionMode)
+ throws GeneralSecurityException {
+ EncryptionHeader header = builder.getHeader();
+ if (existing == null || lastChunk) {
+ String padding = (lastChunk ? "PKCS5Padding" : "NoPadding");
+ existing = getCipher(skey, header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), encryptionMode, padding);
+ }
+
+ byte[] blockKey = new byte[4];
+ LittleEndian.putInt(blockKey, 0, block);
+ byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, header.getBlockSize());
+
+ AlgorithmParameterSpec aps;
+ if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {
+ aps = new RC2ParameterSpec(skey.getEncoded().length*8, iv);
+ } else {
+ aps = new IvParameterSpec(iv);
+ }
+
+ existing.init(encryptionMode, skey, aps);
+
+ return existing;
+ }
+
/**
* 2.3.4.15 Data Encryption (Agile Encryption)
*
* that the StreamSize field of the EncryptedPackage field specifies the number of bytes of
* unencrypted data as specified in section 2.3.4.4.
*/
- private class ChunkedCipherInputStream extends InputStream {
- private int _lastIndex = 0;
- private long _pos = 0;
- private final long _size;
- private final InputStream _stream;
- private byte[] _chunk;
- private Cipher _cipher;
-
- public ChunkedCipherInputStream(DocumentInputStream stream, long size)
- throws GeneralSecurityException {
- EncryptionHeader header = info.getHeader();
- _size = size;
- _stream = stream;
- _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), Cipher.DECRYPT_MODE);
+ private class AgileCipherInputStream extends ChunkedCipherInputStream {
+ public AgileCipherInputStream(DocumentInputStream stream, long size)
+ throws GeneralSecurityException {
+ super(stream, size, 4096);
}
- public int read() throws IOException {
- byte[] b = new byte[1];
- if (read(b) == 1)
- return b[0];
- return -1;
- }
-
- public int read(byte[] b) throws IOException {
- return read(b, 0, b.length);
- }
-
- public int read(byte[] b, int off, int len) throws IOException {
- int total = 0;
-
- if (available() <= 0) return -1;
-
- while (len > 0) {
- if (_chunk == null) {
- try {
- _chunk = nextChunk();
- } catch (GeneralSecurityException e) {
- throw new EncryptedDocumentException(e.getMessage());
- }
- }
- int count = (int)(4096L - (_pos & 0xfff));
- int avail = available();
- if (avail == 0) {
- return total;
- }
- count = Math.min(avail, Math.min(count, len));
- System.arraycopy(_chunk, (int)(_pos & 0xfff), b, off, count);
- off += count;
- len -= count;
- _pos += count;
- if ((_pos & 0xfff) == 0)
- _chunk = null;
- total += count;
- }
-
- return total;
- }
-
- public long skip(long n) throws IOException {
- long start = _pos;
- long skip = Math.min(available(), n);
-
- if ((((_pos + skip) ^ start) & ~0xfff) != 0)
- _chunk = null;
- _pos += skip;
- return skip;
- }
-
- public int available() throws IOException { return (int)(_size - _pos); }
- public void close() throws IOException { _stream.close(); }
- public boolean markSupported() { return false; }
-
- private byte[] nextChunk() throws GeneralSecurityException, IOException {
- int index = (int)(_pos >> 12);
- byte[] blockKey = new byte[4];
- LittleEndian.putInt(blockKey, 0, index);
- EncryptionHeader header = info.getHeader();
- byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, getBlockSizeInBytes());
- 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);
+ // TODO: calculate integrity hmac while reading the stream
+ // for a post-validation of the data
+
+ protected Cipher initCipherForBlock(Cipher cipher, int block)
+ throws GeneralSecurityException {
+ return AgileDecryptor.initCipherForBlock(cipher, block, false, builder, getSecretKey(), Cipher.DECRYPT_MODE);
}
}
-
- protected int getBlockSizeInBytes() {
- return info.getHeader().getBlockSize();
- }
-
- protected int getKeySizeInBytes() {
- return info.getHeader().getKeySize()/8;
- }
}
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;\r
import org.apache.poi.poifs.crypt.EncryptionMode;\r
import org.apache.poi.poifs.crypt.HashAlgorithm;\r
-import org.apache.poi.poifs.filesystem.DocumentInputStream;\r
+import org.apache.poi.util.LittleEndianInput;\r
import org.apache.xmlbeans.XmlException;\r
\r
import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument;\r
AgileDecryptor decryptor;\r
AgileEncryptor encryptor;\r
\r
- public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {\r
+ public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException {\r
this.info = info;\r
\r
- EncryptionDocument ed = parseDescriptor(dis);\r
+ EncryptionDocument ed = parseDescriptor((InputStream)dis);\r
header = new AgileEncryptionHeader(ed);\r
verifier = new AgileEncryptionVerifier(ed);\r
if (info.getVersionMajor() == EncryptionMode.agile.versionMajor\r
==================================================================== */\r
package org.apache.poi.poifs.crypt.agile;\r
\r
-import static org.apache.poi.poifs.crypt.CryptoFunctions.generateIv;\r
import static org.apache.poi.poifs.crypt.CryptoFunctions.getBlock0;\r
import static org.apache.poi.poifs.crypt.CryptoFunctions.getCipher;\r
import static org.apache.poi.poifs.crypt.CryptoFunctions.getMessageDigest;\r
import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword;\r
+import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry;\r
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.getNextBlockSize;\r
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.hashInput;\r
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kCryptoKeyBlock;\r
import java.io.ByteArrayOutputStream;\r
import java.io.File;\r
import java.io.FileInputStream;\r
-import java.io.FileOutputStream;\r
-import java.io.FilterOutputStream;\r
import java.io.IOException;\r
-import java.io.InputStream;\r
import java.io.OutputStream;\r
import java.security.GeneralSecurityException;\r
import java.security.MessageDigest;\r
import java.security.SecureRandom;\r
import java.security.cert.CertificateEncodingException;\r
-import java.security.spec.AlgorithmParameterSpec;\r
import java.util.HashMap;\r
import java.util.Map;\r
import java.util.Random;\r
import javax.crypto.Cipher;\r
import javax.crypto.Mac;\r
import javax.crypto.SecretKey;\r
-import javax.crypto.spec.IvParameterSpec;\r
-import javax.crypto.spec.RC2ParameterSpec;\r
import javax.crypto.spec.SecretKeySpec;\r
\r
import org.apache.poi.EncryptedDocumentException;\r
-import org.apache.poi.poifs.crypt.CipherAlgorithm;\r
+import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream;\r
import org.apache.poi.poifs.crypt.CryptoFunctions;\r
import org.apache.poi.poifs.crypt.DataSpaceMapUtils;\r
-import org.apache.poi.poifs.crypt.EncryptionHeader;\r
import org.apache.poi.poifs.crypt.EncryptionInfo;\r
import org.apache.poi.poifs.crypt.Encryptor;\r
import org.apache.poi.poifs.crypt.HashAlgorithm;\r
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry;\r
+import org.apache.poi.poifs.crypt.standard.EncryptionRecord;\r
import org.apache.poi.poifs.filesystem.DirectoryNode;\r
-import org.apache.poi.poifs.filesystem.POIFSWriterEvent;\r
-import org.apache.poi.poifs.filesystem.POIFSWriterListener;\r
-import org.apache.poi.util.IOUtils;\r
import org.apache.poi.util.LittleEndian;\r
import org.apache.poi.util.LittleEndianByteArrayOutputStream;\r
-import org.apache.poi.util.LittleEndianConsts;\r
-import org.apache.poi.util.LittleEndianOutputStream;\r
-import org.apache.poi.util.TempFile;\r
import org.apache.xmlbeans.XmlOptions;\r
\r
import com.microsoft.schemas.office.x2006.encryption.CTDataIntegrity;\r
\r
public class AgileEncryptor extends Encryptor {\r
private final AgileEncryptionInfoBuilder builder;\r
- @SuppressWarnings("unused")\r
private byte integritySalt[];\r
- private Mac integrityMD;\r
private byte pwHash[];\r
\r
protected AgileEncryptor(AgileEncryptionInfoBuilder builder) {\r
byte encryptedHmacKey[] = cipher.doFinal(filledSalt);\r
header.setEncryptedHmacKey(encryptedHmacKey);\r
\r
- this.integrityMD = CryptoFunctions.getMac(hashAlgo);\r
- this.integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId));\r
-\r
- \r
cipher = Cipher.getInstance("RSA");\r
for (AgileCertificateEntry ace : ver.getCertificates()) {\r
cipher.init(Cipher.ENCRYPT_MODE, ace.x509.getPublicKey());\r
public OutputStream getDataStream(DirectoryNode dir)\r
throws IOException, GeneralSecurityException {\r
// TODO: initialize headers\r
- OutputStream countStream = new ChunkedCipherOutputStream(dir);\r
+ AgileCipherOutputStream countStream = new AgileCipherOutputStream(dir);\r
return countStream;\r
}\r
\r
/**\r
- * 2.3.4.15 Data Encryption (Agile Encryption)\r
+ * Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), \r
+ * which the DataIntegrity element will verify by using the Salt generated in step 2 as the key. \r
+ * Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be \r
+ * used as the message.\r
* \r
- * The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly\r
- * random access while allowing CBC modes to be used in the encryption process.\r
- * The initialization vector for the encryption process MUST be obtained by using the zero-based\r
- * segment number as a blockKey and the binary form of the KeyData.saltValue as specified in\r
- * section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer.\r
- * Data blocks MUST then be encrypted by using the initialization vector and the intermediate key\r
- * obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the\r
- * KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to\r
- * the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note\r
- * that the StreamSize field of the EncryptedPackage field specifies the number of bytes of\r
- * unencrypted data as specified in section 2.3.4.4.\r
- */\r
- private class ChunkedCipherOutputStream extends FilterOutputStream implements POIFSWriterListener {\r
- private long _pos = 0;\r
- private final byte[] _chunk = new byte[4096];\r
- private Cipher _cipher;\r
- private final File fileOut;\r
- protected final DirectoryNode dir;\r
-\r
- public ChunkedCipherOutputStream(DirectoryNode dir) throws IOException {\r
- super(null);\r
- fileOut = TempFile.createTempFile("encrypted_package", "crypt");\r
- this.out = new FileOutputStream(fileOut);\r
- this.dir = dir;\r
- EncryptionHeader header = builder.getHeader();\r
- _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE);\r
- }\r
-\r
- public void write(int b) throws IOException {\r
- write(new byte[]{(byte)b});\r
- }\r
-\r
- public void write(byte[] b) throws IOException {\r
- write(b, 0, b.length);\r
- }\r
-\r
- public void write(byte[] b, int off, int len)\r
- throws IOException {\r
- if (len == 0) return;\r
- \r
- if (len < 0 || b.length < off+len) {\r
- throw new IOException("not enough bytes in your input buffer");\r
- }\r
- \r
- while (len > 0) {\r
- int posInChunk = (int)(_pos & 0xfff);\r
- int nextLen = Math.min(4096-posInChunk, len);\r
- System.arraycopy(b, off, _chunk, posInChunk, nextLen);\r
- _pos += nextLen;\r
- off += nextLen;\r
- len -= nextLen;\r
- if ((_pos & 0xfff) == 0) {\r
- writeChunk();\r
- }\r
- }\r
- }\r
-\r
- private void writeChunk() throws IOException {\r
- EncryptionHeader header = builder.getHeader();\r
- int blockSize = header.getBlockSize();\r
-\r
- int posInChunk = (int)(_pos & 0xfff);\r
- // normally posInChunk is 0, i.e. on the next chunk (-> index-1)\r
- // but if called on close(), posInChunk is somewhere within the chunk data\r
- int index = (int)(_pos >> 12);\r
- if (posInChunk==0) {\r
- index--;\r
- posInChunk = 4096;\r
- } else {\r
- // pad the last chunk\r
- _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE, "PKCS5Padding");\r
- }\r
+ * Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes:\r
+ * 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33.\r
+ **/\r
+ protected void updateIntegrityHMAC(File tmpFile, int oleStreamSize) throws GeneralSecurityException, IOException {\r
+ // as the integrity hmac needs to contain the StreamSize,\r
+ // it's not possible to calculate it on-the-fly while buffering\r
+ // TODO: add stream size parameter to getDataStream()\r
+ AgileEncryptionVerifier ver = builder.getVerifier();\r
+ HashAlgorithm hashAlgo = ver.getHashAlgorithm();\r
+ Mac integrityMD = CryptoFunctions.getMac(hashAlgo);\r
+ integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId));\r
\r
- byte[] blockKey = new byte[4];\r
- LittleEndian.putInt(blockKey, 0, index);\r
- byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, blockSize);\r
- try {\r
- AlgorithmParameterSpec aps;\r
- if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {\r
- aps = new RC2ParameterSpec(getSecretKey().getEncoded().length*8, iv);\r
- } else {\r
- aps = new IvParameterSpec(iv);\r
- }\r
- \r
- _cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), aps);\r
- int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);\r
- out.write(_chunk, 0, ciLen);\r
- } catch (GeneralSecurityException e) {\r
- throw (IOException)new IOException().initCause(e);\r
- }\r
- }\r
+ byte buf[] = new byte[1024];\r
+ LittleEndian.putLong(buf, 0, oleStreamSize);\r
+ integrityMD.update(buf, 0, LittleEndian.LONG_SIZE);\r
\r
- public void close() throws IOException {\r
- writeChunk();\r
- super.close();\r
- writeToPOIFS();\r
- }\r
-\r
- void writeToPOIFS() throws IOException {\r
- DataSpaceMapUtils.addDefaultDataSpace(dir);\r
- \r
- /**\r
- * Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), \r
- * which the DataIntegrity element will verify by using the Salt generated in step 2 as the key. \r
- * Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be \r
- * used as the message.\r
- * \r
- * Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes:\r
- * 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33.\r
- **/\r
- byte buf[] = new byte[4096];\r
- LittleEndian.putLong(buf, 0, _pos);\r
- integrityMD.update(buf, 0, LittleEndianConsts.LONG_SIZE);\r
- \r
- InputStream fis = new FileInputStream(fileOut);\r
- for (int readBytes; (readBytes = fis.read(buf)) != -1; integrityMD.update(buf, 0, readBytes));\r
- fis.close();\r
- \r
- AgileEncryptionHeader header = builder.getHeader(); \r
- int blockSize = header.getBlockSize();\r
- \r
- byte hmacValue[] = integrityMD.doFinal();\r
- byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, header.getBlockSize());\r
- Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE);\r
- try {\r
- byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize));\r
- byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled);\r
- header.setEncryptedHmacValue(encryptedHmacValue);\r
- } catch (GeneralSecurityException e) {\r
- throw new EncryptedDocumentException(e);\r
- }\r
-\r
- createEncryptionInfoEntry(dir);\r
- \r
- int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);\r
- dir.createDocument("EncryptedPackage", oleStreamSize, this);\r
- // TODO: any properties???\r
- }\r
- \r
- public void processPOIFSWriterEvent(POIFSWriterEvent event) {\r
- try {\r
- LittleEndianOutputStream leos = new LittleEndianOutputStream(event.getStream());\r
-\r
- // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data \r
- // encrypted within the EncryptedData field, not including the size of the StreamSize field. \r
- // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this \r
- // value, depending on the block size of the chosen encryption algorithm\r
- leos.writeLong(_pos);\r
-\r
- FileInputStream fis = new FileInputStream(fileOut);\r
- IOUtils.copy(fis, leos);\r
- fis.close();\r
- fileOut.delete();\r
-\r
- leos.close();\r
- } catch (IOException e) {\r
- throw new EncryptedDocumentException(e);\r
- }\r
+ FileInputStream fis = new FileInputStream(tmpFile);\r
+ int readBytes;\r
+ while ((readBytes = fis.read(buf)) != -1) {\r
+ integrityMD.update(buf, 0, readBytes);\r
}\r
- }\r
-\r
- protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {\r
- final CTKeyEncryptor.Uri.Enum passwordUri = \r
- CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD;\r
- final CTKeyEncryptor.Uri.Enum certificateUri = \r
- CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE;\r
+ fis.close();\r
+ \r
+ byte hmacValue[] = integrityMD.doFinal();\r
\r
- AgileEncryptionVerifier ver = builder.getVerifier();\r
AgileEncryptionHeader header = builder.getHeader();\r
+ int blockSize = header.getBlockSize();\r
+ byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, blockSize);\r
+ Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE);\r
+ byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize));\r
+ byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled);\r
+ \r
+ header.setEncryptedHmacValue(encryptedHmacValue);\r
+ }\r
+ \r
+ private final CTKeyEncryptor.Uri.Enum passwordUri = \r
+ CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD;\r
+ private final CTKeyEncryptor.Uri.Enum certificateUri = \r
+ CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE;\r
+ \r
+ protected EncryptionDocument createEncryptionDocument() {\r
+ AgileEncryptionVerifier ver = builder.getVerifier();\r
+ AgileEncryptionHeader header = builder.getHeader(); \r
\r
EncryptionDocument ed = EncryptionDocument.Factory.newInstance();\r
CTEncryption edRoot = ed.addNewEncryption();\r
certData.setCertVerifier(ace.certVerifier);\r
}\r
\r
+ return ed;\r
+ }\r
+ \r
+ protected void marshallEncryptionDocument(EncryptionDocument ed, LittleEndianByteArrayOutputStream os) {\r
XmlOptions xo = new XmlOptions();\r
xo.setCharacterEncoding("UTF-8");\r
Map<String,String> nsMap = new HashMap<String,String>();\r
xo.setSaveSuggestedPrefixes(nsMap);\r
xo.setSaveNamespacesFirst();\r
xo.setSaveAggressiveNamespaces();\r
- // setting standalone doesn't work with xmlbeans-2.3\r
+\r
+ // setting standalone doesn't work with xmlbeans-2.3 & 2.6\r
+ // ed.documentProperties().setStandalone(true);\r
xo.setSaveNoXmlDecl();\r
- \r
ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
- bos.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".getBytes("UTF-8"));\r
- ed.save(bos, xo);\r
+ try {\r
+ bos.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".getBytes("UTF-8"));\r
+ ed.save(bos, xo);\r
+ os.write(bos.toByteArray());\r
+ } catch (IOException e) {\r
+ throw new EncryptedDocumentException("error marshalling encryption info document", e);\r
+ }\r
+ }\r
\r
- final byte buf[] = new byte[5000]; \r
- LittleEndianByteArrayOutputStream leos = new LittleEndianByteArrayOutputStream(buf, 0);\r
- EncryptionInfo info = builder.getInfo();\r
+ protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)\r
+ throws IOException, GeneralSecurityException {\r
+ DataSpaceMapUtils.addDefaultDataSpace(dir);\r
\r
- // EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where \r
- // Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004\r
- leos.writeShort(info.getVersionMajor());\r
- leos.writeShort(info.getVersionMinor());\r
- // Reserved (4 bytes): A value that MUST be 0x00000040\r
- leos.writeInt(info.getEncryptionFlags());\r
- leos.write(bos.toByteArray());\r
- \r
- dir.createDocument("EncryptionInfo", leos.getWriteIndex(), new POIFSWriterListener() {\r
- public void processPOIFSWriterEvent(POIFSWriterEvent event) {\r
- try {\r
- event.getStream().write(buf, 0, event.getLimit());\r
- } catch (IOException e) {\r
- throw new EncryptedDocumentException(e);\r
- }\r
+ final EncryptionInfo info = builder.getInfo();\r
+\r
+ EncryptionRecord er = new EncryptionRecord(){\r
+ public void write(LittleEndianByteArrayOutputStream bos) {\r
+ // EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where \r
+ // Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004\r
+ bos.writeShort(info.getVersionMajor());\r
+ bos.writeShort(info.getVersionMinor());\r
+ // Reserved (4 bytes): A value that MUST be 0x00000040\r
+ bos.writeInt(info.getEncryptionFlags());\r
+\r
+ EncryptionDocument ed = createEncryptionDocument();\r
+ marshallEncryptionDocument(ed, bos);\r
}\r
- });\r
+ };\r
+ \r
+ createEncryptionEntry(dir, "EncryptionInfo", er);\r
}\r
+ \r
+ \r
+ /**\r
+ * 2.3.4.15 Data Encryption (Agile Encryption)\r
+ * \r
+ * The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly\r
+ * random access while allowing CBC modes to be used in the encryption process.\r
+ * The initialization vector for the encryption process MUST be obtained by using the zero-based\r
+ * segment number as a blockKey and the binary form of the KeyData.saltValue as specified in\r
+ * section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer.\r
+ * Data blocks MUST then be encrypted by using the initialization vector and the intermediate key\r
+ * obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the\r
+ * KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to\r
+ * the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note\r
+ * that the StreamSize field of the EncryptedPackage field specifies the number of bytes of\r
+ * unencrypted data as specified in section 2.3.4.4.\r
+ */\r
+ private class AgileCipherOutputStream extends ChunkedCipherOutputStream {\r
+ public AgileCipherOutputStream(DirectoryNode dir) throws IOException, GeneralSecurityException {\r
+ super(dir, 4096);\r
+ }\r
+ \r
+ @Override\r
+ protected Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk)\r
+ throws GeneralSecurityException {\r
+ return AgileDecryptor.initCipherForBlock(existing, block, lastChunk, builder, getSecretKey(), Cipher.ENCRYPT_MODE);\r
+ }\r
+\r
+ @Override\r
+ protected void calculateChecksum(File fileOut, int oleStreamSize)\r
+ throws GeneralSecurityException, IOException {\r
+ // integrityHMAC needs to be updated before the encryption document is created\r
+ updateIntegrityHMAC(fileOut, oleStreamSize); \r
+ }\r
+ \r
+ @Override\r
+ protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)\r
+ throws IOException, GeneralSecurityException {\r
+ AgileEncryptor.this.createEncryptionInfoEntry(dir, tmpFile);\r
+ }\r
+ }\r
+\r
}\r
@Parameter(value = 2)\r
public ChainingMode cm;\r
\r
- @Parameters\r
+ @Parameters(name="{0} {1} {2}")\r
public static Collection<Object[]> data() {\r
CipherAlgorithm caList[] = { CipherAlgorithm.aes128, CipherAlgorithm.aes192, CipherAlgorithm.aes256, CipherAlgorithm.rc2, CipherAlgorithm.des, CipherAlgorithm.des3 };\r
HashAlgorithm haList[] = { HashAlgorithm.sha1, HashAlgorithm.sha256, HashAlgorithm.sha384, HashAlgorithm.sha512, HashAlgorithm.md5 };\r
ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
\r
POIFSFileSystem fsEnc = new POIFSFileSystem();\r
- EncryptionInfo infoEnc = new EncryptionInfo(fsEnc, EncryptionMode.agile, ca, ha, -1, -1, cm);\r
+ EncryptionInfo infoEnc = new EncryptionInfo(EncryptionMode.agile, ca, ha, -1, -1, cm);\r
Encryptor enc = infoEnc.getEncryptor();\r
enc.confirmPassword("foobaa");\r
OutputStream os = enc.getDataStream(fsEnc);\r
import java.util.zip.ZipInputStream;\r
\r
import org.apache.poi.POIDataSamples;\r
+import org.apache.poi.poifs.filesystem.DirectoryNode;\r
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;\r
import org.apache.poi.poifs.filesystem.POIFSFileSystem;\r
import org.apache.poi.util.IOUtils;\r
\r
d.verifyPassword(Decryptor.DEFAULT_PASSWORD);\r
\r
- zipOk(fs, d);\r
+ zipOk(fs.getRoot(), d);\r
}\r
\r
@Test\r
\r
assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));\r
\r
- zipOk(fs, d);\r
+ zipOk(fs.getRoot(), d);\r
}\r
\r
- private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {\r
- ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));\r
+ private void zipOk(DirectoryNode root, Decryptor d) throws IOException, GeneralSecurityException {\r
+ ZipInputStream zin = new ZipInputStream(d.getDataStream(root));\r
\r
while (true) {\r
ZipEntry entry = zin.getNextEntry();\r
- if (entry==null) {\r
- break;\r
- }\r
-\r
- while (zin.available()>0) {\r
- zin.skip(zin.available());\r
- }\r
+ if (entry==null) break;\r
+ // crc32 is checked within zip-stream\r
+ if (entry.isDirectory()) continue;\r
+ zin.skip(entry.getSize());\r
+ byte buf[] = new byte[10];\r
+ int readBytes = zin.read(buf);\r
+ // zin.available() doesn't work for entries\r
+ assertEquals("size failed for "+entry.getName(), -1, readBytes);\r
}\r
\r
zin.close();\r
==================================================================== */\r
package org.apache.poi.poifs.crypt;\r
\r
-import static org.hamcrest.core.IsEqual.equalTo;\r
+import static org.junit.Assert.assertArrayEquals;\r
import static org.junit.Assert.assertEquals;\r
-import static org.junit.Assert.assertThat;\r
import static org.junit.Assert.assertTrue;\r
\r
import java.io.ByteArrayInputStream;\r
\r
public class TestEncryptor {\r
@Test\r
- public void testAgileEncryption() throws Exception {\r
+ public void binaryRC4Encryption() throws Exception {\r
+ // please contribute a real sample file, which is binary rc4 encrypted\r
+ // ... at least the output can be opened in Excel Viewer \r
+ String password = "pass";\r
+\r
+ InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleMultiCell.xlsx");\r
+ ByteArrayOutputStream payloadExpected = new ByteArrayOutputStream();\r
+ IOUtils.copy(is, payloadExpected);\r
+ is.close();\r
+ \r
+ POIFSFileSystem fs = new POIFSFileSystem();\r
+ EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4);\r
+ Encryptor enc = ei.getEncryptor();\r
+ enc.confirmPassword(password);\r
+ \r
+ OutputStream os = enc.getDataStream(fs.getRoot());\r
+ payloadExpected.writeTo(os);\r
+ os.close();\r
+ \r
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
+ fs.writeFilesystem(bos);\r
+ \r
+ fs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()));\r
+ ei = new EncryptionInfo(fs);\r
+ Decryptor dec = ei.getDecryptor();\r
+ boolean b = dec.verifyPassword(password);\r
+ assertTrue(b);\r
+ \r
+ ByteArrayOutputStream payloadActual = new ByteArrayOutputStream();\r
+ is = dec.getDataStream(fs.getRoot());\r
+ IOUtils.copy(is,payloadActual);\r
+ is.close();\r
+ \r
+ assertArrayEquals(payloadExpected.toByteArray(), payloadActual.toByteArray());\r
+ }\r
+ \r
+ @Test\r
+ public void agileEncryption() throws Exception {\r
int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");\r
Assume.assumeTrue("Please install JCE Unlimited Strength Jurisdiction Policy files for AES 256", maxKeyLen == 2147483647);\r
\r
\r
POIFSFileSystem fs = new POIFSFileSystem();\r
EncryptionInfo infoActual = new EncryptionInfo(\r
- fs, EncryptionMode.agile\r
+ EncryptionMode.agile\r
, infoExpected.getVerifier().getCipherAlgorithm()\r
, infoExpected.getVerifier().getHashAlgorithm()\r
, infoExpected.getHeader().getKeySize()\r
\r
AgileEncryptionHeader aehExpected = (AgileEncryptionHeader)infoExpected.getHeader();\r
AgileEncryptionHeader aehActual = (AgileEncryptionHeader)infoActual.getHeader();\r
- assertThat(aehExpected.getEncryptedHmacKey(), equalTo(aehActual.getEncryptedHmacKey()));\r
+ assertArrayEquals(aehExpected.getEncryptedHmacKey(), aehActual.getEncryptedHmacKey());\r
assertEquals(decPackLenExpected, decPackLenActual);\r
- assertThat(payloadExpected, equalTo(payloadActual));\r
- assertThat(encPackExpected, equalTo(encPackActual));\r
+ assertArrayEquals(payloadExpected, payloadActual);\r
+ assertArrayEquals(encPackExpected, encPackActual);\r
}\r
\r
@Test\r
- public void testStandardEncryption() throws Exception {\r
+ public void standardEncryption() throws Exception {\r
File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx");\r
String pass = "solrcell";\r
\r
\r
POIFSFileSystem fs = new POIFSFileSystem();\r
EncryptionInfo infoActual = new EncryptionInfo(\r
- fs, EncryptionMode.standard\r
+ EncryptionMode.standard\r
, infoExpected.getVerifier().getCipherAlgorithm()\r
, infoExpected.getVerifier().getHashAlgorithm()\r
, infoExpected.getHeader().getKeySize()\r
Encryptor e = Encryptor.getInstance(infoActual);\r
e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, null);\r
\r
- assertThat(infoExpected.getVerifier().getEncryptedVerifier(), equalTo(infoActual.getVerifier().getEncryptedVerifier()));\r
- assertThat(infoExpected.getVerifier().getEncryptedVerifierHash(), equalTo(infoActual.getVerifier().getEncryptedVerifierHash()));\r
+ assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifier(), infoActual.getVerifier().getEncryptedVerifier());\r
+ assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifierHash(), infoActual.getVerifier().getEncryptedVerifierHash());\r
\r
// now we use a newly generated salt/verifier and check\r
// if the file content is still the same \r
\r
fs = new POIFSFileSystem();\r
infoActual = new EncryptionInfo(\r
- fs, EncryptionMode.standard\r
+ EncryptionMode.standard\r
, infoExpected.getVerifier().getCipherAlgorithm()\r
, infoExpected.getVerifier().getHashAlgorithm()\r
, infoExpected.getHeader().getKeySize()\r
nfs.close();\r
byte payloadActual[] = bos.toByteArray(); \r
\r
- assertThat(payloadExpected, equalTo(payloadActual));\r
+ assertArrayEquals(payloadExpected, payloadActual);\r
}\r
\r
@Test\r
@Ignore\r
- public void testInPlaceRewrite() throws Exception {\r
+ public void inPlaceRewrite() throws Exception {\r
File f = TempFile.createTempFile("protected_agile", ".docx");\r
// File f = new File("protected_agile.docx");\r
FileOutputStream fos = new FileOutputStream(f);\r
\r
\r
private void listEntry(DocumentNode de, String ext, String path) throws IOException {\r
- path += "\\" + de.getName().replace('\u0006', '_');\r
+ path += "\\" + de.getName().replaceAll("[\\p{Cntrl}]", "_");\r
System.out.println(ext+": "+path+" ("+de.getSize()+" bytes)");\r
\r
- String name = de.getName().replace('\u0006', '_');\r
+ String name = de.getName().replaceAll("[\\p{Cntrl}]", "_");\r
\r
InputStream is = ((DirectoryNode)de.getParent()).createDocumentInputStream(de);\r
FileOutputStream fos = new FileOutputStream("solr."+name+"."+ext);\r
package org.apache.poi.hslf;
-import java.io.FileNotFoundException;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
-import org.apache.poi.hslf.record.CurrentUserAtom;
+import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;
import org.apache.poi.hslf.record.DocumentEncryptionAtom;
import org.apache.poi.hslf.record.PersistPtrHolder;
+import org.apache.poi.hslf.record.PositionDependentRecord;
import org.apache.poi.hslf.record.Record;
import org.apache.poi.hslf.record.UserEditAtom;
+import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
+import org.apache.poi.poifs.crypt.Decryptor;
+import org.apache.poi.poifs.crypt.EncryptionInfo;
+import org.apache.poi.poifs.crypt.Encryptor;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptor;
+import org.apache.poi.util.BitField;
+import org.apache.poi.util.Internal;
+import org.apache.poi.util.LittleEndian;
/**
- * This class provides helper functions for determining if a
- * PowerPoint document is Encrypted.
- * In future, it may also provide Encryption and Decryption
- * functions, but first we'd need to figure out how
- * PowerPoint encryption is really done!
- *
- * @author Nick Burch
+ * This class provides helper functions for encrypted PowerPoint documents.
*/
+@Internal
+public class EncryptedSlideShow {
+ DocumentEncryptionAtom dea;
+ CryptoAPIEncryptor enc = null;
+ CryptoAPIDecryptor dec = null;
+ Cipher cipher = null;
+ CipherOutputStream cyos = null;
+
+ private static final BitField fieldRecInst = new BitField(0xFFF0);
+
+ protected EncryptedSlideShow(DocumentEncryptionAtom dea) {
+ this.dea = dea;
+ }
+
+ protected EncryptedSlideShow(byte[] docstream, NavigableMap<Integer,Record> recordMap) {
+ // check for DocumentEncryptionAtom, which would be at the last offset
+ // need to ignore already set UserEdit and PersistAtoms
+ UserEditAtom userEditAtomWithEncryption = null;
+ for (Map.Entry<Integer, Record> me : recordMap.descendingMap().entrySet()) {
+ Record r = me.getValue();
+ if (!(r instanceof UserEditAtom)) continue;
+ UserEditAtom uea = (UserEditAtom)r;
+ if (uea.getEncryptSessionPersistIdRef() != -1) {
+ userEditAtomWithEncryption = uea;
+ break;
+ }
+ }
+
+ if (userEditAtomWithEncryption == null) {
+ dea = null;
+ return;
+ }
+
+ Record r = recordMap.get(userEditAtomWithEncryption.getPersistPointersOffset());
+ assert(r instanceof PersistPtrHolder);
+ PersistPtrHolder ptr = (PersistPtrHolder)r;
+
+ Integer encOffset = ptr.getSlideLocationsLookup().get(userEditAtomWithEncryption.getEncryptSessionPersistIdRef());
+ assert(encOffset != null);
+
+ r = recordMap.get(encOffset);
+ if (r == null) {
+ r = Record.buildRecordAtOffset(docstream, encOffset);
+ recordMap.put(encOffset, r);
+ }
+ assert(r instanceof DocumentEncryptionAtom);
+ this.dea = (DocumentEncryptionAtom)r;
+
+ CryptoAPIDecryptor dec = (CryptoAPIDecryptor)dea.getEncryptionInfo().getDecryptor();
+ String pass = Biff8EncryptionKey.getCurrentUserPassword();
+ if(!dec.verifyPassword(pass != null ? pass : Decryptor.DEFAULT_PASSWORD)) {
+ throw new EncryptedPowerPointFileException("PowerPoint file is encrypted. The correct password needs to be set via Biff8EncryptionKey.setCurrentUserPassword()");
+ }
+ }
+
+ public DocumentEncryptionAtom getDocumentEncryptionAtom() {
+ return dea;
+ }
+
+ protected void setPersistId(int persistId) {
+ if (enc != null && dec != null) {
+ throw new EncryptedPowerPointFileException("Use instance either for en- or decryption");
+ }
+
+ try {
+ if (enc != null) cipher = enc.initCipherForBlock(cipher, persistId);
+ if (dec != null) cipher = dec.initCipherForBlock(cipher, persistId);
+ } catch (GeneralSecurityException e) {
+ throw new EncryptedPowerPointFileException(e);
+ }
+ }
+
+ protected void decryptInit() {
+ if (dec != null) return;
+ EncryptionInfo ei = dea.getEncryptionInfo();
+ dec = (CryptoAPIDecryptor)ei.getDecryptor();
+ }
+
+ protected void encryptInit() {
+ if (enc != null) return;
+ EncryptionInfo ei = dea.getEncryptionInfo();
+ enc = (CryptoAPIEncryptor)ei.getEncryptor();
+ }
+
+
+
+ protected OutputStream encryptRecord(OutputStream plainStream, int persistId, Record record) {
+ boolean isPlain = (dea == null
+ || record instanceof UserEditAtom
+ || record instanceof PersistPtrHolder
+ || record instanceof DocumentEncryptionAtom
+ );
+ if (isPlain) return plainStream;
+
+ encryptInit();
+ setPersistId(persistId);
+
+ if (cyos == null) {
+ cyos = new CipherOutputStream(plainStream, cipher);
+ }
+ return cyos;
+ }
+
+ protected void decryptRecord(byte[] docstream, int persistId, int offset) {
+ if (dea == null) return;
+
+ decryptInit();
+ setPersistId(persistId);
+
+ try {
+ // decrypt header and read length to be decrypted
+ cipher.update(docstream, offset, 8, docstream, offset);
+ // decrypt the rest of the record
+ int rlen = (int)LittleEndian.getUInt(docstream, offset+4);
+ cipher.update(docstream, offset+8, rlen, docstream, offset+8);
+ } catch (GeneralSecurityException e) {
+ throw new CorruptPowerPointFileException(e);
+ }
+ }
+
+ protected void decryptPicture(byte[] pictstream, int offset) {
+ if (dea == null) return;
+
+ decryptInit();
+ setPersistId(0);
+
+ try {
+ // decrypt header and read length to be decrypted
+ cipher.doFinal(pictstream, offset, 8, pictstream, offset);
+ int recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
+ int recType = LittleEndian.getUShort(pictstream, offset+2);
+ int rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
+ offset += 8;
+ int endOffset = offset + rlen;
+
+ if (recType == 0xF007) {
+ // TOOD: get a real example file ... to actual test the FBSE entry
+ // not sure where the foDelay block is
+
+ // File BLIP Store Entry (FBSE)
+ cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btWin32
+ offset++;
+ cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btMacOS
+ offset++;
+ cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid
+ offset += 16;
+ cipher.doFinal(pictstream, offset, 2, pictstream, offset); // tag
+ offset += 2;
+ cipher.doFinal(pictstream, offset, 4, pictstream, offset); // size
+ offset += 4;
+ cipher.doFinal(pictstream, offset, 4, pictstream, offset); // cRef
+ offset += 4;
+ cipher.doFinal(pictstream, offset, 4, pictstream, offset); // foDelay
+ offset += 4;
+ cipher.doFinal(pictstream, offset+0, 1, pictstream, offset+0); // unused1
+ cipher.doFinal(pictstream, offset+1, 1, pictstream, offset+1); // cbName
+ cipher.doFinal(pictstream, offset+2, 1, pictstream, offset+2); // unused2
+ cipher.doFinal(pictstream, offset+3, 1, pictstream, offset+3); // unused3
+ int cbName = LittleEndian.getUShort(pictstream, offset+1);
+ offset += 4;
+ if (cbName > 0) {
+ cipher.doFinal(pictstream, offset, cbName, pictstream, offset); // nameData
+ offset += cbName;
+ }
+ if (offset == endOffset) {
+ return; // no embedded blip
+ }
+ // fall through, read embedded blip now
+
+ // update header data
+ cipher.doFinal(pictstream, offset, 8, pictstream, offset);
+ recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
+ recType = LittleEndian.getUShort(pictstream, offset+2);
+ rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
+ offset += 8;
+ }
+
+ int rgbUidCnt = (recInst == 0x217 || recInst == 0x3D5 || recInst == 0x46B || recInst == 0x543 ||
+ recInst == 0x6E1 || recInst == 0x6E3 || recInst == 0x6E5 || recInst == 0x7A9) ? 2 : 1;
+
+ for (int i=0; i<rgbUidCnt; i++) {
+ cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid 1/2
+ offset += 16;
+ }
+
+ if (recType == 0xF01A || recType == 0XF01B || recType == 0XF01C) {
+ cipher.doFinal(pictstream, offset, 34, pictstream, offset); // metafileHeader
+ offset += 34;
+ } else {
+ cipher.doFinal(pictstream, offset, 1, pictstream, offset); // tag
+ offset += 1;
+ }
+
+ int blipLen = endOffset - offset;
+ cipher.doFinal(pictstream, offset, blipLen, pictstream, offset);
+ } catch (GeneralSecurityException e) {
+ throw new CorruptPowerPointFileException(e);
+ }
+ }
+
+ protected void encryptPicture(byte[] pictstream, int offset) {
+ if (dea == null) return;
+
+ encryptInit();
+ setPersistId(0);
+
+ try {
+ int recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
+ int recType = LittleEndian.getUShort(pictstream, offset+2);
+ int rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
+ cipher.doFinal(pictstream, offset, 8, pictstream, offset);
+ offset += 8;
+ int endOffset = offset + rlen;
+
+ if (recType == 0xF007) {
+ // TOOD: get a real example file ... to actual test the FBSE entry
+ // not sure where the foDelay block is
+
+ // File BLIP Store Entry (FBSE)
+ cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btWin32
+ offset++;
+ cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btMacOS
+ offset++;
+ cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid
+ offset += 16;
+ cipher.doFinal(pictstream, offset, 2, pictstream, offset); // tag
+ offset += 2;
+ cipher.doFinal(pictstream, offset, 4, pictstream, offset); // size
+ offset += 4;
+ cipher.doFinal(pictstream, offset, 4, pictstream, offset); // cRef
+ offset += 4;
+ cipher.doFinal(pictstream, offset, 4, pictstream, offset); // foDelay
+ offset += 4;
+ int cbName = LittleEndian.getUShort(pictstream, offset+1);
+ cipher.doFinal(pictstream, offset+0, 1, pictstream, offset+0); // unused1
+ cipher.doFinal(pictstream, offset+1, 1, pictstream, offset+1); // cbName
+ cipher.doFinal(pictstream, offset+2, 1, pictstream, offset+2); // unused2
+ cipher.doFinal(pictstream, offset+3, 1, pictstream, offset+3); // unused3
+ offset += 4;
+ if (cbName > 0) {
+ cipher.doFinal(pictstream, offset, cbName, pictstream, offset); // nameData
+ offset += cbName;
+ }
+ if (offset == endOffset) {
+ return; // no embedded blip
+ }
+ // fall through, read embedded blip now
+
+ // update header data
+ recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
+ recType = LittleEndian.getUShort(pictstream, offset+2);
+ rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
+ cipher.doFinal(pictstream, offset, 8, pictstream, offset);
+ offset += 8;
+ }
+
+ int rgbUidCnt = (recInst == 0x217 || recInst == 0x3D5 || recInst == 0x46B || recInst == 0x543 ||
+ recInst == 0x6E1 || recInst == 0x6E3 || recInst == 0x6E5 || recInst == 0x7A9) ? 2 : 1;
+
+ for (int i=0; i<rgbUidCnt; i++) {
+ cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid 1/2
+ offset += 16;
+ }
+
+ if (recType == 0xF01A || recType == 0XF01B || recType == 0XF01C) {
+ cipher.doFinal(pictstream, offset, 34, pictstream, offset); // metafileHeader
+ offset += 34;
+ } else {
+ cipher.doFinal(pictstream, offset, 1, pictstream, offset); // tag
+ offset += 1;
+ }
+
+ int blipLen = endOffset - offset;
+ cipher.doFinal(pictstream, offset, blipLen, pictstream, offset);
+ } catch (GeneralSecurityException e) {
+ throw new CorruptPowerPointFileException(e);
+ }
+ }
+
+ protected Record[] updateEncryptionRecord(Record records[]) {
+ String password = Biff8EncryptionKey.getCurrentUserPassword();
+ if (password == null) {
+ if (dea == null) {
+ // no password given, no encryption record exits -> done
+ return records;
+ } else {
+ // need to remove password data
+ dea = null;
+ return removeEncryptionRecord(records);
+ }
+ } else {
+ // create password record
+ if (dea == null) {
+ dea = new DocumentEncryptionAtom();
+ }
+ EncryptionInfo ei = dea.getEncryptionInfo();
+ byte salt[] = ei.getVerifier().getSalt();
+ Encryptor enc = ei.getEncryptor();
+ if (salt == null) {
+ enc.confirmPassword(password);
+ } else {
+ byte verifier[] = ei.getDecryptor().getVerifier();
+ enc.confirmPassword(password, null, null, verifier, salt, null);
+ }
+
+ // move EncryptionRecord to last slide position
+ records = normalizeRecords(records);
+ return addEncryptionRecord(records, dea);
+ }
+ }
+
+ /**
+ * remove duplicated UserEditAtoms and merge PersistPtrHolder.
+ * Before this method is called, make sure that the offsets are correct,
+ * i.e. call {@link HSLFSlideShow#updateAndWriteDependantRecords(OutputStream, Map)}
+ */
+ protected static Record[] normalizeRecords(Record records[]) {
+ // http://msdn.microsoft.com/en-us/library/office/gg615594(v=office.14).aspx
+ // repeated slideIds can be overwritten, i.e. ignored
+
+ UserEditAtom uea = null;
+ PersistPtrHolder pph = null;
+ TreeMap<Integer,Integer> slideLocations = new TreeMap<Integer,Integer>();
+ TreeMap<Integer,Record> recordMap = new TreeMap<Integer,Record>();
+ List<Integer> obsoleteOffsets = new ArrayList<Integer>();
+ int duplicatedCount = 0;
+ for (Record r : records) {
+ assert(r instanceof PositionDependentRecord);
+ PositionDependentRecord pdr = (PositionDependentRecord)r;
+ if (pdr instanceof UserEditAtom) {
+ uea = (UserEditAtom)pdr;
+ continue;
+ }
+
+ if (pdr instanceof PersistPtrHolder) {
+ if (pph != null) {
+ duplicatedCount++;
+ }
+ pph = (PersistPtrHolder)pdr;
+ for (Map.Entry<Integer,Integer> me : pph.getSlideLocationsLookup().entrySet()) {
+ Integer oldOffset = slideLocations.put(me.getKey(), me.getValue());
+ if (oldOffset != null) obsoleteOffsets.add(oldOffset);
+ }
+ continue;
+ }
+
+ recordMap.put(pdr.getLastOnDiskOffset(), r);
+ }
+ recordMap.put(pph.getLastOnDiskOffset(), pph);
+ recordMap.put(uea.getLastOnDiskOffset(), uea);
+
+ assert(uea != null && pph != null && uea.getPersistPointersOffset() == pph.getLastOnDiskOffset());
+
+ if (duplicatedCount == 0 && obsoleteOffsets.isEmpty()) {
+ return records;
+ }
+
+ uea.setLastUserEditAtomOffset(0);
+ pph.clear();
+ for (Map.Entry<Integer,Integer> me : slideLocations.entrySet()) {
+ pph.addSlideLookup(me.getKey(), me.getValue());
+ }
+
+ for (Integer oldOffset : obsoleteOffsets) {
+ recordMap.remove(oldOffset);
+ }
+
+ return recordMap.values().toArray(new Record[recordMap.size()]);
+ }
+
+
+ protected static Record[] removeEncryptionRecord(Record records[]) {
+ int deaSlideId = -1;
+ int deaOffset = -1;
+ PersistPtrHolder ptr = null;
+ UserEditAtom uea = null;
+ List<Record> recordList = new ArrayList<Record>();
+ for (Record r : records) {
+ if (r instanceof DocumentEncryptionAtom) {
+ deaOffset = ((DocumentEncryptionAtom)r).getLastOnDiskOffset();
+ continue;
+ } else if (r instanceof UserEditAtom) {
+ uea = (UserEditAtom)r;
+ deaSlideId = uea.getEncryptSessionPersistIdRef();
+ uea.setEncryptSessionPersistIdRef(-1);
+ } else if (r instanceof PersistPtrHolder) {
+ ptr = (PersistPtrHolder)r;
+ }
+ recordList.add(r);
+ }
+
+ assert(ptr != null);
+ if (deaSlideId == -1 && deaOffset == -1) return records;
+
+ TreeMap<Integer,Integer> tm = new TreeMap<Integer,Integer>(ptr.getSlideLocationsLookup());
+ ptr.clear();
+ int maxSlideId = -1;
+ for (Map.Entry<Integer,Integer> me : tm.entrySet()) {
+ if (me.getKey() == deaSlideId || me.getValue() == deaOffset) continue;
+ ptr.addSlideLookup(me.getKey(), me.getValue());
+ maxSlideId = Math.max(me.getKey(), maxSlideId);
+ }
+
+ uea.setMaxPersistWritten(maxSlideId);
+
+ records = recordList.toArray(new Record[recordList.size()]);
+
+ return records;
+ }
+
+
+ protected static Record[] addEncryptionRecord(Record records[], DocumentEncryptionAtom dea) {
+ assert(dea != null);
+ int ueaIdx = -1, ptrIdx = -1, deaIdx = -1, idx = -1;
+ for (Record r : records) {
+ idx++;
+ if (r instanceof UserEditAtom) ueaIdx = idx;
+ else if (r instanceof PersistPtrHolder) ptrIdx = idx;
+ else if (r instanceof DocumentEncryptionAtom) deaIdx = idx;
+ }
+ assert(ueaIdx != -1 && ptrIdx != -1 && ptrIdx < ueaIdx);
+ if (deaIdx != -1) {
+ DocumentEncryptionAtom deaOld = (DocumentEncryptionAtom)records[deaIdx];
+ dea.setLastOnDiskOffset(deaOld.getLastOnDiskOffset());
+ records[deaIdx] = dea;
+ return records;
+ } else {
+ PersistPtrHolder ptr = (PersistPtrHolder)records[ptrIdx];
+ UserEditAtom uea = ((UserEditAtom)records[ueaIdx]);
+ dea.setLastOnDiskOffset(ptr.getLastOnDiskOffset()-1);
+ int nextSlideId = uea.getMaxPersistWritten()+1;
+ ptr.addSlideLookup(nextSlideId, ptr.getLastOnDiskOffset()-1);
+ uea.setEncryptSessionPersistIdRef(nextSlideId);
+ uea.setMaxPersistWritten(nextSlideId);
+
+ Record newRecords[] = new Record[records.length+1];
+ if (ptrIdx > 0) System.arraycopy(records, 0, newRecords, 0, ptrIdx);
+ if (ptrIdx < records.length-1) System.arraycopy(records, ptrIdx, newRecords, ptrIdx+1, records.length-ptrIdx);
+ newRecords[ptrIdx] = dea;
+ return newRecords;
+ }
+ }
-public final class EncryptedSlideShow
-{
- /**
- * Check to see if a HSLFSlideShow represents an encrypted
- * PowerPoint document, or not
- * @param hss The HSLFSlideShow to check
- * @return true if encrypted, otherwise false
- */
- public static boolean checkIfEncrypted(HSLFSlideShow hss) {
- // Easy way to check - contains a stream
- // "EncryptedSummary"
- try {
- hss.getPOIFSDirectory().getEntry("EncryptedSummary");
- return true;
- } catch(FileNotFoundException fnfe) {
- // Doesn't have encrypted properties
- }
-
- // If they encrypted the document but not the properties,
- // it's harder.
- // We need to see what the last record pointed to by the
- // first PersistPrtHolder is - if it's a
- // DocumentEncryptionAtom, then the file's Encrypted
- DocumentEncryptionAtom dea = fetchDocumentEncryptionAtom(hss);
- if(dea != null) {
- return true;
- }
- return false;
- }
-
- /**
- * Return the DocumentEncryptionAtom for a HSLFSlideShow, or
- * null if there isn't one.
- * @return a DocumentEncryptionAtom, or null if there isn't one
- */
- public static DocumentEncryptionAtom fetchDocumentEncryptionAtom(HSLFSlideShow hss) {
- // Will be the last Record pointed to by the
- // first PersistPrtHolder, if there is one
-
- CurrentUserAtom cua = hss.getCurrentUserAtom();
- if(cua.getCurrentEditOffset() != 0) {
- // Check it's not past the end of the file
- if(cua.getCurrentEditOffset() > hss.getUnderlyingBytes().length) {
- throw new CorruptPowerPointFileException("The CurrentUserAtom claims that the offset of last edit details are past the end of the file");
- }
-
- // Grab the details of the UserEditAtom there
- // If the record's messed up, we could AIOOB
- Record r = null;
- try {
- r = Record.buildRecordAtOffset(
- hss.getUnderlyingBytes(),
- (int)cua.getCurrentEditOffset()
- );
- } catch (ArrayIndexOutOfBoundsException e) {
- return null;
- }
- if(r == null) { return null; }
- if(! (r instanceof UserEditAtom)) { return null; }
- UserEditAtom uea = (UserEditAtom)r;
-
- // Now get the PersistPtrHolder
- Record r2 = Record.buildRecordAtOffset(
- hss.getUnderlyingBytes(),
- uea.getPersistPointersOffset()
- );
- if(! (r2 instanceof PersistPtrHolder)) { return null; }
- PersistPtrHolder pph = (PersistPtrHolder)r2;
-
- // Now get the last record
- int[] slideIds = pph.getKnownSlideIDs();
- int maxSlideId = -1;
- for(int i=0; i<slideIds.length; i++) {
- if(slideIds[i] > maxSlideId) { maxSlideId = slideIds[i]; }
- }
- if(maxSlideId == -1) { return null; }
-
- int offset = (
- (Integer)pph.getSlideLocationsLookup().get(
- Integer.valueOf(maxSlideId)
- ) ).intValue();
- Record r3 = Record.buildRecordAtOffset(
- hss.getUnderlyingBytes(),
- offset
- );
-
- // If we have a DocumentEncryptionAtom, it'll be this one
- if(r3 instanceof DocumentEncryptionAtom) {
- return (DocumentEncryptionAtom)r3;
- }
- }
-
- return null;
- }
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.TreeMap;
import org.apache.poi.POIDocument;
+import org.apache.poi.hpsf.PropertySet;
import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
-import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;
import org.apache.poi.hslf.exceptions.HSLFException;
import org.apache.poi.hslf.record.CurrentUserAtom;
+import org.apache.poi.hslf.record.DocumentEncryptionAtom;
import org.apache.poi.hslf.record.ExOleObjStg;
import org.apache.poi.hslf.record.PersistPtrHolder;
import org.apache.poi.hslf.record.PersistRecord;
import org.apache.poi.hslf.record.UserEditAtom;
import org.apache.poi.hslf.usermodel.ObjectData;
import org.apache.poi.hslf.usermodel.PictureData;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptor;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
// PowerPoint stream
readPowerPointStream();
- // Check to see if we have an encrypted document,
- // bailing out if we do
- boolean encrypted = EncryptedSlideShow.checkIfEncrypted(this);
- if(encrypted) {
- throw new EncryptedPowerPointFileException("Encrypted PowerPoint files are not supported");
- }
-
// Now, build records based on the PowerPoint stream
buildRecords();
NavigableMap<Integer,Record> records = new TreeMap<Integer,Record>(); // offset -> record
Map<Integer,Integer> persistIds = new HashMap<Integer,Integer>(); // offset -> persistId
initRecordOffsets(docstream, usrOffset, records, persistIds);
+ EncryptedSlideShow decryptData = new EncryptedSlideShow(docstream, records);
for (Map.Entry<Integer,Record> entry : records.entrySet()) {
Integer offset = entry.getKey();
if (record == null) {
// all plain records have been already added,
// only new records need to be decrypted (tbd #35897)
+ decryptData.decryptRecord(docstream, persistId, offset);
record = Record.buildRecordAtOffset(docstream, offset);
entry.setValue(record);
}
}
}
+ public DocumentEncryptionAtom getDocumentEncryptionAtom() {
+ for (Record r : _records) {
+ if (r instanceof DocumentEncryptionAtom) {
+ return (DocumentEncryptionAtom)r;
+ }
+ }
+ return null;
+ }
+
+
/**
* Find the "Current User" stream, and load it
*/
private void readOtherStreams() {
// Currently, there aren't any
}
+
/**
* Find and read in pictures contained in this presentation.
* This is lazily called as and when we want to touch pictures.
// if the presentation doesn't contain pictures - will use a null set instead
if (!directory.hasEntry("Pictures")) return;
+
+ EncryptedSlideShow decryptData = new EncryptedSlideShow(getDocumentEncryptionAtom());
DocumentEntry entry = (DocumentEntry)directory.getEntry("Pictures");
byte[] pictstream = new byte[entry.getSize()];
// An empty picture record (length 0) will take up 8 bytes
while (pos <= (pictstream.length-8)) {
int offset = pos;
+
+ decryptData.decryptPicture(pictstream, offset);
// Image signature
int signature = LittleEndian.getUShort(pictstream, pos);
pos += imgsize;
}
}
-
+
+ /**
+ * remove duplicated UserEditAtoms and merge PersistPtrHolder, i.e.
+ * remove document edit history
+ */
+ public void normalizeRecords() {
+ try {
+ updateAndWriteDependantRecords(null, null);
+ } catch (IOException e) {
+ throw new CorruptPowerPointFileException(e);
+ }
+ _records = EncryptedSlideShow.normalizeRecords(_records);
+ }
+
+
/**
* This is a helper functions, which is needed for adding new position dependent records
* or finally write the slideshow to a file.
// records are going to end up, in the new scheme
// (Annoyingly, some powerpoint files have PersistPtrHolders
// that reference slides after the PersistPtrHolder)
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ UserEditAtom usr = null;
+ PersistPtrHolder ptr = null;
+ CountingOS cos = new CountingOS();
for (Record record : _records) {
- if(record instanceof PositionDependentRecord) {
- PositionDependentRecord pdr = (PositionDependentRecord)record;
- int oldPos = pdr.getLastOnDiskOffset();
- int newPos = baos.size();
- pdr.setLastOnDiskOffset(newPos);
- if (oldPos != UNSET_OFFSET) {
- // new records don't need a mapping, as they aren't in a relation yet
- oldToNewPositions.put(Integer.valueOf(oldPos),Integer.valueOf(newPos));
- }
+ // all top level records are position dependent
+ assert(record instanceof PositionDependentRecord);
+ PositionDependentRecord pdr = (PositionDependentRecord)record;
+ int oldPos = pdr.getLastOnDiskOffset();
+ int newPos = cos.size();
+ pdr.setLastOnDiskOffset(newPos);
+ if (oldPos != UNSET_OFFSET) {
+ // new records don't need a mapping, as they aren't in a relation yet
+ oldToNewPositions.put(oldPos,newPos);
+ }
+
+ // Grab interesting records as they come past
+ // this will only save the very last record of each type
+ RecordTypes.Type saveme = null;
+ int recordType = (int)record.getRecordType();
+ if (recordType == RecordTypes.PersistPtrIncrementalBlock.typeID) {
+ saveme = RecordTypes.PersistPtrIncrementalBlock;
+ ptr = (PersistPtrHolder)pdr;
+ } else if (recordType == RecordTypes.UserEditAtom.typeID) {
+ saveme = RecordTypes.UserEditAtom;
+ usr = (UserEditAtom)pdr;
+ }
+ if (interestingRecords != null && saveme != null) {
+ interestingRecords.put(saveme,pdr);
}
// Dummy write out, so the position winds on properly
- record.writeOut(baos);
+ record.writeOut(cos);
}
- baos = null;
- // For now, we're only handling PositionDependentRecord's that
- // happen at the top level.
- // In future, we'll need the handle them everywhere, but that's
- // a bit trickier
- UserEditAtom usr = null;
- for (Record record : _records) {
- if (record instanceof PositionDependentRecord) {
- // We've already figured out their new location, and
- // told them that
- // Tell them of the positions of the other records though
- PositionDependentRecord pdr = (PositionDependentRecord)record;
- pdr.updateOtherRecordReferences(oldToNewPositions);
-
- // Grab interesting records as they come past
- // this will only save the very last record of each type
- RecordTypes.Type saveme = null;
- int recordType = (int)record.getRecordType();
- if (recordType == RecordTypes.PersistPtrIncrementalBlock.typeID) {
- saveme = RecordTypes.PersistPtrIncrementalBlock;
- } else if (recordType == RecordTypes.UserEditAtom.typeID) {
- saveme = RecordTypes.UserEditAtom;
- usr = (UserEditAtom)pdr;
- }
- if (interestingRecords != null && saveme != null) {
- interestingRecords.put(saveme,pdr);
- }
- }
+ assert(usr != null && ptr != null);
+
+ Map<Integer,Integer> persistIds = new HashMap<Integer,Integer>();
+ for (Map.Entry<Integer,Integer> entry : ptr.getSlideLocationsLookup().entrySet()) {
+ persistIds.put(oldToNewPositions.get(entry.getValue()), entry.getKey());
+ }
+
+ EncryptedSlideShow encData = new EncryptedSlideShow(getDocumentEncryptionAtom());
+
+ for (Record record : _records) {
+ assert(record instanceof PositionDependentRecord);
+ // We've already figured out their new location, and
+ // told them that
+ // Tell them of the positions of the other records though
+ PositionDependentRecord pdr = (PositionDependentRecord)record;
+ Integer persistId = persistIds.get(pdr.getLastOnDiskOffset());
+ if (persistId == null) persistId = 0;
+
+ // For now, we're only handling PositionDependentRecord's that
+ // happen at the top level.
+ // In future, we'll need the handle them everywhere, but that's
+ // a bit trickier
+ pdr.updateOtherRecordReferences(oldToNewPositions);
// Whatever happens, write out that record tree
if (os != null) {
- record.writeOut(os);
+ record.writeOut(encData.encryptRecord(os, persistId, record));
}
}
}
currentUser.setCurrentEditOffset(usr.getLastOnDiskOffset());
}
-
+
/**
* Writes out the slideshow file the is represented by an instance
* of this class.
* the passed in OutputStream
*/
public void write(OutputStream out, boolean preserveNodes) throws IOException {
+ // read properties and pictures, with old encryption settings where appropriate
+ if(_pictures == null) {
+ readPictures();
+ }
+ getDocumentSummaryInformation();
+
+ // set new encryption settings
+ EncryptedSlideShow encryptedSS = new EncryptedSlideShow(getDocumentEncryptionAtom());
+ _records = encryptedSS.updateEncryptionRecord(_records);
+
// Get a new Filesystem to write into
POIFSFileSystem outFS = new POIFSFileSystem();
// Write out the Property Streams
writeProperties(outFS, writtenEntries);
-
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ BufAccessBAOS baos = new BufAccessBAOS();
// For position dependent records, hold where they were and now are
// As we go along, update, and hand over, to any Position Dependent
updateAndWriteDependantRecords(baos, null);
// Update our cached copy of the bytes that make up the PPT stream
- _docstream = baos.toByteArray();
+ _docstream = new byte[baos.size()];
+ System.arraycopy(baos.getBuf(), 0, _docstream, 0, baos.size());
// Write the PPT stream into the POIFS layer
- ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ ByteArrayInputStream bais = new ByteArrayInputStream(_docstream);
outFS.createDocument(bais,"PowerPoint Document");
writtenEntries.add("PowerPoint Document");
+
+ currentUser.setEncrypted(encryptedSS.getDocumentEncryptionAtom() != null);
currentUser.writeToFS(outFS);
writtenEntries.add("Current User");
- // Write any pictures, into another stream
- if(_pictures == null) {
- readPictures();
- }
if (_pictures.size() > 0) {
- ByteArrayOutputStream pict = new ByteArrayOutputStream();
+ BufAccessBAOS pict = new BufAccessBAOS();
for (PictureData p : _pictures) {
+ int offset = pict.size();
p.write(pict);
+ encryptedSS.encryptPicture(pict.getBuf(), offset);
}
outFS.createDocument(
- new ByteArrayInputStream(pict.toByteArray()), "Pictures"
+ new ByteArrayInputStream(pict.getBuf(), 0, pict.size()), "Pictures"
);
writtenEntries.add("Pictures");
}
outFS.writeFilesystem(out);
}
+ /**
+ * For a given named property entry, either return it or null if
+ * if it wasn't found
+ *
+ * @param setName The property to read
+ * @return The value of the given property or null if it wasn't found.
+ */
+ protected PropertySet getPropertySet(String setName) {
+ DocumentEncryptionAtom dea = getDocumentEncryptionAtom();
+ return (dea == null)
+ ? super.getPropertySet(setName)
+ : super.getPropertySet(setName, dea.getEncryptionInfo());
+ }
- /* ******************* adding methods follow ********************* */
+ /**
+ * Writes out the standard Documment Information Properties (HPSF)
+ * @param outFS the POIFSFileSystem to write the properties into
+ * @param writtenEntries a list of POIFS entries to add the property names too
+ *
+ * @throws IOException if an error when writing to the
+ * {@link POIFSFileSystem} occurs
+ */
+ protected void writeProperties(POIFSFileSystem outFS, List<String> writtenEntries) throws IOException {
+ super.writeProperties(outFS, writtenEntries);
+ DocumentEncryptionAtom dea = getDocumentEncryptionAtom();
+ if (dea != null) {
+ CryptoAPIEncryptor enc = (CryptoAPIEncryptor)dea.getEncryptionInfo().getEncryptor();
+ try {
+ enc.getDataStream(outFS.getRoot()); // ignore OutputStream
+ } catch (IOException e) {
+ throw e;
+ } catch (GeneralSecurityException e) {
+ throw new IOException(e);
+ }
+ }
+ }
+
+ /* ******************* adding methods follow ********************* */
/**
* Adds a new root level record, at the end, but before the last
}
return _objects;
}
+
+
+ private static class BufAccessBAOS extends ByteArrayOutputStream {
+ public byte[] getBuf() {
+ return buf;
+ }
+ }
+
+ private static class CountingOS extends OutputStream {
+ int count = 0;
+ public void write(int b) throws IOException {
+ count++;
+ }
+
+ public void write(byte[] b) throws IOException {
+ count += b.length;
+ }
+
+ public void write(byte[] b, int off, int len) throws IOException {
+ count += len;
+ }
+
+ public int size() {
+ return count;
+ }
+ }
}
package org.apache.poi.hslf.dev;
-import org.apache.poi.hslf.*;
-import org.apache.poi.hslf.record.*;
+import java.io.ByteArrayOutputStream;
+import java.util.Map;
+
+import org.apache.poi.hslf.HSLFSlideShow;
+import org.apache.poi.hslf.record.Document;
+import org.apache.poi.hslf.record.Notes;
+import org.apache.poi.hslf.record.NotesAtom;
+import org.apache.poi.hslf.record.PersistPtrHolder;
+import org.apache.poi.hslf.record.PositionDependentRecord;
+import org.apache.poi.hslf.record.Record;
+import org.apache.poi.hslf.record.Slide;
+import org.apache.poi.hslf.record.SlideAtom;
+import org.apache.poi.hslf.record.SlideListWithText;
+import org.apache.poi.hslf.record.SlidePersistAtom;
import org.apache.poi.hslf.usermodel.SlideShow;
-
import org.apache.poi.util.LittleEndian;
-import java.io.*;
-import java.util.Hashtable;
-
/**
* Gets all the different things that have Slide IDs (of sorts)
* in them, and displays them, so you can try to guess what they
// Check the sheet offsets
int[] sheetIDs = pph.getKnownSlideIDs();
- Hashtable sheetOffsets = pph.getSlideLocationsLookup();
+ Map<Integer,Integer> sheetOffsets = pph.getSlideLocationsLookup();
for(int j=0; j<sheetIDs.length; j++) {
- Integer id = Integer.valueOf(sheetIDs[j]);
- Integer offset = (Integer)sheetOffsets.get(id);
+ Integer id = sheetIDs[j];
+ Integer offset = sheetOffsets.get(id);
System.out.println(" Knows about sheet " + id);
System.out.println(" That sheet lives at " + offset);
package org.apache.poi.hslf.dev;
import java.io.ByteArrayOutputStream;
-import java.util.Hashtable;
+import java.util.Map;
import org.apache.poi.hslf.HSLFSlideShow;
-import org.apache.poi.hslf.record.*;
+import org.apache.poi.hslf.record.CurrentUserAtom;
+import org.apache.poi.hslf.record.PersistPtrHolder;
+import org.apache.poi.hslf.record.PositionDependentRecord;
+import org.apache.poi.hslf.record.Record;
+import org.apache.poi.hslf.record.UserEditAtom;
import org.apache.poi.util.LittleEndian;
/**
// Check the sheet offsets
int[] sheetIDs = pph.getKnownSlideIDs();
- Hashtable sheetOffsets = pph.getSlideLocationsLookup();
+ Map<Integer,Integer> sheetOffsets = pph.getSlideLocationsLookup();
for(int j=0; j<sheetIDs.length; j++) {
- Integer id = Integer.valueOf(sheetIDs[j]);
- Integer offset = (Integer)sheetOffsets.get(id);
+ Integer id = sheetIDs[j];
+ Integer offset = sheetOffsets.get(id);
System.out.println(" Knows about sheet " + id);
System.out.println(" That sheet lives at " + offset);
public CorruptPowerPointFileException(String s) {
super(s);
}
+
+ public CorruptPowerPointFileException(String s, Throwable t) {
+ super(s,t);
+ }
+
+ public CorruptPowerPointFileException(Throwable t) {
+ super(t);
+ }
}
public EncryptedPowerPointFileException(String s) {
super(s);
}
+
+ public EncryptedPowerPointFileException(String s, Throwable t) {
+ super(s, t);
+ }
+
+ public EncryptedPowerPointFileException(Throwable t) {
+ super(t);
+ }
}
package org.apache.poi.hslf.record;
-import java.io.*;
-import org.apache.poi.poifs.filesystem.*;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
+import org.apache.poi.hslf.exceptions.OldPowerPointFormatException;
+import org.apache.poi.poifs.filesystem.DirectoryNode;
+import org.apache.poi.poifs.filesystem.DocumentEntry;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
import org.apache.poi.util.StringUtil;
-import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
-import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;
-import org.apache.poi.hslf.exceptions.OldPowerPointFormatException;
/**
public static final byte[] atomHeader = new byte[] { 0, 0, -10, 15 };
/** The PowerPoint magic number for a non-encrypted file */
public static final byte[] headerToken = new byte[] { 95, -64, -111, -29 };
- /** The PowerPoint magic number for an encrytpted file */
+ /** The PowerPoint magic number for an encrypted file */
public static final byte[] encHeaderToken = new byte[] { -33, -60, -47, -13 };
/** The Powerpoint 97 version, major and minor numbers */
public static final byte[] ppt97FileVer = new byte[] { 8, 00, -13, 03, 03, 00 };
/** Only correct after reading in or writing out */
private byte[] _contents;
+
+ /** Flag for encryption state of the whole file */
+ private boolean isEncrypted;
/* ********************* getter/setter follows *********************** */
public String getLastEditUsername() { return lastEditUser; }
public void setLastEditUsername(String u) { lastEditUser = u; }
+ public boolean isEncrypted() { return isEncrypted; }
+ public void setEncrypted(boolean isEncrypted) { this.isEncrypted = isEncrypted; }
+
/* ********************* real code follows *************************** */
releaseVersion = 8;
currentEditOffset = 0;
lastEditUser = "Apache POI";
+ isEncrypted = false;
}
/**
*/
private void init() {
// First up is the size, in 4 bytes, which is fixed
- // Then is the header - check for encrypted
- if(_contents[12] == encHeaderToken[0] &&
- _contents[13] == encHeaderToken[1] &&
- _contents[14] == encHeaderToken[2] &&
- _contents[15] == encHeaderToken[3]) {
- throw new EncryptedPowerPointFileException("The CurrentUserAtom specifies that the document is encrypted");
- }
+ // Then is the header
+ isEncrypted = (LittleEndian.getInt(encHeaderToken) == LittleEndian.getInt(_contents,12));
+
// Grab the edit offset
currentEditOffset = LittleEndian.getUInt(_contents,16);
LittleEndian.putInt(_contents,8,20);
// Now the ppt un-encrypted header token (4 bytes)
- System.arraycopy(headerToken,0,_contents,12,4);
+ System.arraycopy((isEncrypted ? encHeaderToken : headerToken),0,_contents,12,4);
// Now the current edit offset
LittleEndian.putInt(_contents,16,(int)currentEditOffset);
package org.apache.poi.hslf.record;
-import org.apache.poi.util.StringUtil;
-
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.util.Hashtable;
+
+import org.apache.poi.poifs.crypt.CipherAlgorithm;
+import org.apache.poi.poifs.crypt.EncryptionInfo;
+import org.apache.poi.poifs.crypt.EncryptionMode;
+import org.apache.poi.poifs.crypt.HashAlgorithm;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader;
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionVerifier;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianByteArrayOutputStream;
+import org.apache.poi.util.LittleEndianInputStream;
/**
* A Document Encryption Atom (type 12052). Holds information
*
* @author Nick Burch
*/
-public final class DocumentEncryptionAtom extends RecordAtom {
+public final class DocumentEncryptionAtom extends PositionDependentRecordAtom {
+ private static long _type = 12052l;
private byte[] _header;
- private static long _type = 12052l;
-
- private byte[] data;
- private String encryptionProviderName;
+ private EncryptionInfo ei;
/**
* For the Document Encryption Atom
*/
- protected DocumentEncryptionAtom(byte[] source, int start, int len) {
+ protected DocumentEncryptionAtom(byte[] source, int start, int len) throws IOException {
// Get the header
_header = new byte[8];
System.arraycopy(source,start,_header,0,8);
- // Grab everything else, for now
- data = new byte[len-8];
- System.arraycopy(source, start+8, data, 0, len-8);
-
- // Grab the provider, from byte 8+44 onwards
- // It's a null terminated Little Endian String
- int endPos = -1;
- int pos = start + 8+44;
- while(pos < (start+len) && endPos < 0) {
- if(source[pos] == 0 && source[pos+1] == 0) {
- // Hit the end
- endPos = pos;
- }
- pos += 2;
- }
- pos = start + 8+44;
- int stringLen = (endPos-pos) / 2;
- encryptionProviderName = StringUtil.getFromUnicodeLE(source, pos, stringLen);
+ ByteArrayInputStream bis = new ByteArrayInputStream(source, start+8, len-8);
+ LittleEndianInputStream leis = new LittleEndianInputStream(bis);
+ ei = new EncryptionInfo(leis, true);
}
+ public DocumentEncryptionAtom() {
+ _header = new byte[8];
+ LittleEndian.putShort(_header, 0, (short)0x000F);
+ LittleEndian.putShort(_header, 2, (short)_type);
+ // record length not yet known ...
+
+ ei = new EncryptionInfo(EncryptionMode.cryptoAPI);
+ }
+
+ /**
+ * Initializes the encryption settings
+ *
+ * @param keyBits see {@link CipherAlgorithm#rc4} for allowed values, use -1 for default size
+ */
+ public void initializeEncryptionInfo(int keyBits) {
+ ei = new EncryptionInfo(EncryptionMode.cryptoAPI, CipherAlgorithm.rc4, HashAlgorithm.sha1, keyBits, -1, null);
+ }
+
/**
* Return the length of the encryption key, in bits
*/
public int getKeyLength() {
- return data[28];
+ return ei.getHeader().getKeySize();
}
/**
* Return the name of the encryption provider used
*/
public String getEncryptionProviderName() {
- return encryptionProviderName;
+ return ei.getHeader().getCspName();
}
-
+ /**
+ * @return the {@link EncryptionInfo} object for details about encryption settings
+ */
+ public EncryptionInfo getEncryptionInfo() {
+ return ei;
+ }
+
+
/**
* We are of type 12052
*/
* to disk
*/
public void writeOut(OutputStream out) throws IOException {
- // Header
- out.write(_header);
// Data
- out.write(data);
+ byte data[] = new byte[1024];
+ LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(data, 0);
+ bos.writeShort(ei.getVersionMajor());
+ bos.writeShort(ei.getVersionMinor());
+ bos.writeInt(ei.getEncryptionFlags());
+
+ ((CryptoAPIEncryptionHeader)ei.getHeader()).write(bos);
+ ((CryptoAPIEncryptionVerifier)ei.getVerifier()).write(bos);
+
+ // Header
+ LittleEndian.putInt(_header, 4, bos.getWriteIndex());
+ out.write(_header);
+ out.write(data, 0, bos.getWriteIndex());
}
+
+ public void updateOtherRecordReferences(Hashtable<Integer,Integer> oldToNewReferencesLookup) {
+
+ }
}
package org.apache.poi.hslf.record;
-import org.apache.poi.util.LittleEndian;
-import org.apache.poi.util.POILogger;
-
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
-import java.util.Enumeration;
import java.util.Hashtable;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
+import org.apache.poi.util.BitField;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.POILogger;
/**
* General holder for PersistPtrFullBlock and PersistPtrIncrementalBlock
* that knows about a given slide to find the right location
*/
private Hashtable<Integer,Integer> _slideLocations;
- /**
- * Holds the lookup from slide id to where their offset is
- * held inside _ptrData. Used when writing out, and updating
- * the positions of the slides
- */
- private Hashtable<Integer,Integer> _slideOffsetDataLocation;
+
+ private static final BitField persistIdFld = new BitField(0X000FFFFF);
+ private static final BitField cntPersistFld = new BitField(0XFFF00000);
+
+ /**
+ * Return the value we were given at creation, be it 6001 or 6002
+ */
+ public long getRecordType() { return _type; }
/**
* Get the list of slides that this PersistPtrHolder knows about.
*/
public int[] getKnownSlideIDs() {
int[] ids = new int[_slideLocations.size()];
- Enumeration<Integer> e = _slideLocations.keys();
- for(int i=0; i<ids.length; i++) {
- Integer id = e.nextElement();
- ids[i] = id.intValue();
+ int i = 0;
+ for (Integer slideId : _slideLocations.keySet()) {
+ ids[i++] = slideId;
}
return ids;
}
public Hashtable<Integer,Integer> getSlideLocationsLookup() {
return _slideLocations;
}
+
/**
* Get the lookup from slide numbers to their offsets inside
* _ptrData, used when adding or moving slides.
+ *
+ * @deprecated since POI 3.11, not supported anymore
*/
+ @Deprecated
public Hashtable<Integer,Integer> getSlideOffsetDataLocationsLookup() {
- return _slideOffsetDataLocation;
- }
-
- /**
- * Adds a new slide, notes or similar, to be looked up by this.
- * For now, won't look for the most optimal on disk representation.
- */
- public void addSlideLookup(int slideID, int posOnDisk) {
- // PtrData grows by 8 bytes:
- // 4 bytes for the new info block
- // 4 bytes for the slide offset
- byte[] newPtrData = new byte[_ptrData.length + 8];
- System.arraycopy(_ptrData,0,newPtrData,0,_ptrData.length);
-
- // Add to the slide location lookup hash
- _slideLocations.put(Integer.valueOf(slideID), Integer.valueOf(posOnDisk));
- // Add to the ptrData offset lookup hash
- _slideOffsetDataLocation.put(Integer.valueOf(slideID),
- Integer.valueOf(_ptrData.length + 4));
-
- // Build the info block
- // First 20 bits = offset number = slide ID
- // Remaining 12 bits = offset count = 1
- int infoBlock = slideID;
- infoBlock += (1 << 20);
-
- // Write out the data for this
- LittleEndian.putInt(newPtrData,newPtrData.length-8,infoBlock);
- LittleEndian.putInt(newPtrData,newPtrData.length-4,posOnDisk);
-
- // Save the new ptr data
- _ptrData = newPtrData;
-
- // Update the atom header
- LittleEndian.putInt(_header,4,newPtrData.length);
+ throw new UnsupportedOperationException("PersistPtrHolder.getSlideOffsetDataLocationsLookup() is not supported since 3.12-Beta1");
}
/**
// count * 32 bit offsets
// Repeat as many times as you have data
_slideLocations = new Hashtable<Integer,Integer>();
- _slideOffsetDataLocation = new Hashtable<Integer,Integer>();
_ptrData = new byte[len-8];
System.arraycopy(source,start+8,_ptrData,0,_ptrData.length);
int pos = 0;
while(pos < _ptrData.length) {
- // Grab the info field
- long info = LittleEndian.getUInt(_ptrData,pos);
+ // Grab the info field
+ int info = LittleEndian.getInt(_ptrData,pos);
// First 20 bits = offset number
// Remaining 12 bits = offset count
- int offset_count = (int)(info >> 20);
- int offset_no = (int)(info - (offset_count << 20));
-//System.out.println("Info is " + info + ", count is " + offset_count + ", number is " + offset_no);
-
+ int offset_no = persistIdFld.getValue(info);
+ int offset_count = cntPersistFld.getValue(info);
+
// Wind on by the 4 byte info header
pos += 4;
// Grab the offsets for each of the sheets
for(int i=0; i<offset_count; i++) {
int sheet_no = offset_no + i;
- long sheet_offset = LittleEndian.getUInt(_ptrData,pos);
- _slideLocations.put(Integer.valueOf(sheet_no), Integer.valueOf((int)sheet_offset));
- _slideOffsetDataLocation.put(Integer.valueOf(sheet_no), Integer.valueOf(pos));
+ int sheet_offset = (int)LittleEndian.getUInt(_ptrData,pos);
+ _slideLocations.put(sheet_no, sheet_offset);
// Wind on by 4 bytes per sheet found
pos += 4;
}
}
- /**
- * Return the value we were given at creation, be it 6001 or 6002
- */
- public long getRecordType() { return _type; }
+ /**
+ * remove all slide references
+ *
+ * Convenience method provided, for easier reviewing of invocations
+ */
+ public void clear() {
+ _slideLocations.clear();
+ }
+
+ /**
+ * Adds a new slide, notes or similar, to be looked up by this.
+ */
+ public void addSlideLookup(int slideID, int posOnDisk) {
+ if (_slideLocations.containsKey(slideID)) {
+ throw new CorruptPowerPointFileException("A record with persistId "+slideID+" already exists.");
+ }
+
+ _slideLocations.put(slideID, posOnDisk);
+ }
/**
* At write-out time, update the references to the sheets to their
* new positions
*/
public void updateOtherRecordReferences(Hashtable<Integer,Integer> oldToNewReferencesLookup) {
- int[] slideIDs = getKnownSlideIDs();
-
// Loop over all the slides we know about
// Find where they used to live, and where they now live
- // Then, update the right bit of _ptrData with their new location
- for(int i=0; i<slideIDs.length; i++) {
- Integer id = Integer.valueOf(slideIDs[i]);
- Integer oldPos = (Integer)_slideLocations.get(id);
- Integer newPos = (Integer)oldToNewReferencesLookup.get(oldPos);
-
- if(newPos == null) {
- logger.log(POILogger.WARN, "Couldn't find the new location of the \"slide\" with id " + id + " that used to be at " + oldPos);
- logger.log(POILogger.WARN, "Not updating the position of it, you probably won't be able to find it any more (if you ever could!)");
- newPos = oldPos;
- }
-
- // Write out the new location
- Integer dataOffset = (Integer)_slideOffsetDataLocation.get(id);
- LittleEndian.putInt(_ptrData,dataOffset.intValue(),newPos.intValue());
-
- // Update our hashtable
- _slideLocations.remove(id);
- _slideLocations.put(id,newPos);
- }
+ for (Map.Entry<Integer,Integer> me : _slideLocations.entrySet()) {
+ Integer oldPos = me.getValue();
+ Integer newPos = oldToNewReferencesLookup.get(oldPos);
+
+ if (newPos == null) {
+ Integer id = me.getKey();
+ logger.log(POILogger.WARN, "Couldn't find the new location of the \"slide\" with id " + id + " that used to be at " + oldPos);
+ logger.log(POILogger.WARN, "Not updating the position of it, you probably won't be able to find it any more (if you ever could!)");
+ } else {
+ me.setValue(newPos);
+ }
+ }
}
+ private void normalizePersistDirectory() {
+ TreeMap<Integer,Integer> orderedSlideLocations = new TreeMap<Integer,Integer>(_slideLocations);
+
+ @SuppressWarnings("resource")
+ BufAccessBAOS bos = new BufAccessBAOS();
+ byte intbuf[] = new byte[4];
+ int lastPersistEntry = -1;
+ int lastSlideId = -1;
+ for (Map.Entry<Integer,Integer> me : orderedSlideLocations.entrySet()) {
+ int nextSlideId = me.getKey();
+ int offset = me.getValue();
+ try {
+ // Building the info block
+ // First 20 bits = offset number = slide ID (persistIdFld, i.e. first slide ID of a continuous group)
+ // Remaining 12 bits = offset count = 1 (cntPersistFld, i.e. continuous entries in a group)
+
+ if (lastSlideId+1 == nextSlideId) {
+ // use existing PersistDirectoryEntry, need to increase entry count
+ assert(lastPersistEntry != -1);
+ int infoBlock = LittleEndian.getInt(bos.getBuf(), lastPersistEntry);
+ int entryCnt = cntPersistFld.getValue(infoBlock);
+ infoBlock = cntPersistFld.setValue(infoBlock, entryCnt+1);
+ LittleEndian.putInt(bos.getBuf(), lastPersistEntry, infoBlock);
+ } else {
+ // start new PersistDirectoryEntry
+ lastPersistEntry = bos.size();
+ int infoBlock = persistIdFld.setValue(0, nextSlideId);
+ infoBlock = cntPersistFld.setValue(infoBlock, 1);
+ LittleEndian.putInt(intbuf, 0, infoBlock);
+ bos.write(intbuf);
+ }
+ // Add to the ptrData offset lookup hash
+ LittleEndian.putInt(intbuf, 0, offset);
+ bos.write(intbuf);
+ lastSlideId = nextSlideId;
+ } catch (IOException e) {
+ // ByteArrayOutputStream is very unlikely throwing a IO exception (maybe because of OOM ...)
+ throw new RuntimeException(e);
+ }
+ }
+
+ // Save the new ptr data
+ _ptrData = bos.toByteArray();
+
+ // Update the atom header
+ LittleEndian.putInt(_header,4,bos.size());
+ }
+
/**
* Write the contents of the record back, so it can be written
* to disk
*/
public void writeOut(OutputStream out) throws IOException {
+ normalizePersistDirectory();
out.write(_header);
out.write(_ptrData);
}
+
+ private static class BufAccessBAOS extends ByteArrayOutputStream {
+ public byte[] getBuf() {
+ return buf;
+ }
+ }
}
*/
public static void writeLittleEndian(int i,OutputStream o) throws IOException {
byte[] bi = new byte[4];
- LittleEndian.putInt(bi,i);
+ LittleEndian.putInt(bi,0,i);
o.write(bi);
}
/**
*/
public static void writeLittleEndian(short s,OutputStream o) throws IOException {
byte[] bs = new byte[2];
- LittleEndian.putShort(bs,s);
+ LittleEndian.putShort(bs,0,s);
o.write(bs);
}
package org.apache.poi.hslf.record;
import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianConsts;
+
import java.io.IOException;
import java.io.OutputStream;
import java.util.Hashtable;
private byte[] _header;
private static long _type = 4085l;
- private byte[] reserved;
+ private short unused;
private int lastViewedSlideID;
private int pptVersion;
private int docPersistRef;
private int maxPersistWritten;
private short lastViewType;
+ private int encryptSessionPersistIdRef = -1;
// Somewhat user facing getters
public int getLastViewedSlideID() { return lastViewedSlideID; }
public int getPersistPointersOffset() { return persistPointersOffset; }
public int getDocPersistRef() { return docPersistRef; }
public int getMaxPersistWritten() { return maxPersistWritten; }
+ public int getEncryptSessionPersistIdRef() { return encryptSessionPersistIdRef; }
// More scary internal setters
public void setLastUserEditAtomOffset(int offset) { lastUserEditAtomOffset = offset; }
public void setPersistPointersOffset(int offset) { persistPointersOffset = offset; }
public void setLastViewType(short type) { lastViewType=type; }
- public void setMaxPersistWritten(int max) { maxPersistWritten=max; }
+ public void setMaxPersistWritten(int max) { maxPersistWritten=max; }
+ public void setEncryptSessionPersistIdRef(int id) {
+ encryptSessionPersistIdRef=id;
+ LittleEndian.putInt(_header,4,(id == -1 ? 28 : 32));
+ }
/* *************** record code follows ********************** */
// Sanity Checking
if(len < 34) { len = 34; }
+ int offset = start;
// Get the header
_header = new byte[8];
- System.arraycopy(source,start,_header,0,8);
+ System.arraycopy(source,offset,_header,0,8);
+ offset += 8;
// Get the last viewed slide ID
- lastViewedSlideID = LittleEndian.getInt(source,start+0+8);
+ lastViewedSlideID = LittleEndian.getInt(source,offset);
+ offset += LittleEndianConsts.INT_SIZE;
// Get the PPT version
- pptVersion = LittleEndian.getInt(source,start+4+8);
+ pptVersion = LittleEndian.getInt(source,offset);
+ offset += LittleEndianConsts.INT_SIZE;
// Get the offset to the previous incremental save's UserEditAtom
// This will be the byte offset on disk where the previous one
// starts, or 0 if this is the first one
- lastUserEditAtomOffset = LittleEndian.getInt(source,start+8+8);
+ lastUserEditAtomOffset = LittleEndian.getInt(source,offset);
+ offset += LittleEndianConsts.INT_SIZE;
// Get the offset to the persist pointers
// This will be the byte offset on disk where the preceding
// PersistPtrFullBlock or PersistPtrIncrementalBlock starts
- persistPointersOffset = LittleEndian.getInt(source,start+12+8);
+ persistPointersOffset = LittleEndian.getInt(source,offset);
+ offset += LittleEndianConsts.INT_SIZE;
// Get the persist reference for the document persist object
// Normally seems to be 1
- docPersistRef = LittleEndian.getInt(source,start+16+8);
+ docPersistRef = LittleEndian.getInt(source,offset);
+ offset += LittleEndianConsts.INT_SIZE;
// Maximum number of persist objects written
- maxPersistWritten = LittleEndian.getInt(source,start+20+8);
+ maxPersistWritten = LittleEndian.getInt(source,offset);
+ offset += LittleEndianConsts.INT_SIZE;
// Last view type
- lastViewType = LittleEndian.getShort(source,start+24+8);
+ lastViewType = LittleEndian.getShort(source,offset);
+ offset += LittleEndianConsts.SHORT_SIZE;
+
+ // unused
+ unused = LittleEndian.getShort(source,offset);
+ offset += LittleEndianConsts.SHORT_SIZE;
// There might be a few more bytes, which are a reserved field
- reserved = new byte[len-26-8];
- System.arraycopy(source,start+26+8,reserved,0,reserved.length);
+ if (offset-start<len) {
+ encryptSessionPersistIdRef = LittleEndian.getInt(source,offset);
+ offset += LittleEndianConsts.INT_SIZE;
+ }
+
+ assert(offset-start == len);
}
/**
writeLittleEndian(docPersistRef,out);
writeLittleEndian(maxPersistWritten,out);
writeLittleEndian(lastViewType,out);
-
- // Reserved fields
- out.write(reserved);
+ writeLittleEndian(unused,out);
+ if (encryptSessionPersistIdRef != -1) {
+ // optional field
+ writeLittleEndian(encryptSessionPersistIdRef,out);
+ }
}
}
package org.apache.poi.hslf.record;
-import junit.framework.Test;
-import junit.framework.TestSuite;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
/**
* Collects all tests from the package <tt>org.apache.poi.hslf.record</tt>.
*
* @author Josh Micich
*/
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ TestAnimationInfoAtom.class,
+ TestCString.class,
+ TestColorSchemeAtom.class,
+ TestComment2000.class,
+ TestComment2000Atom.class,
+ TestCurrentUserAtom.class,
+ TestDocument.class,
+ TestDocumentAtom.class,
+ TestDocumentEncryptionAtom.class,
+ TestExControl.class,
+ TestExHyperlink.class,
+ TestExHyperlinkAtom.class,
+ TestExMediaAtom.class,
+ TestExObjList.class,
+ TestExObjListAtom.class,
+ TestExOleObjAtom.class,
+ TestExOleObjStg.class,
+ TestExVideoContainer.class,
+ TestFontCollection.class,
+ TestHeadersFootersAtom.class,
+ TestHeadersFootersContainer.class,
+ TestInteractiveInfo.class,
+ TestInteractiveInfoAtom.class,
+ TestNotesAtom.class,
+ TestRecordContainer.class,
+ TestRecordTypes.class,
+ TestSlideAtom.class,
+ TestSlidePersistAtom.class,
+ TestSound.class,
+ TestStyleTextPropAtom.class,
+ TestTextBytesAtom.class,
+ TestTextCharsAtom.class,
+ TestTextHeaderAtom.class,
+ TestTextRulerAtom.class,
+ TestTextSpecInfoAtom.class,
+ TestTxInteractiveInfoAtom.class,
+ TestTxMasterStyleAtom.class,
+ TestUserEditAtom.class
+})
public class AllHSLFRecordTests {
-
- public static Test suite() {
- TestSuite result = new TestSuite(AllHSLFRecordTests.class.getName());
- result.addTestSuite(TestAnimationInfoAtom.class);
- result.addTestSuite(TestCString.class);
- result.addTestSuite(TestColorSchemeAtom.class);
- result.addTestSuite(TestComment2000.class);
- result.addTestSuite(TestComment2000Atom.class);
- result.addTestSuite(TestCurrentUserAtom.class);
- result.addTestSuite(TestDocument.class);
- result.addTestSuite(TestDocumentAtom.class);
- result.addTestSuite(TestDocumentEncryptionAtom.class);
- result.addTestSuite(TestExControl.class);
- result.addTestSuite(TestExHyperlink.class);
- result.addTestSuite(TestExHyperlinkAtom.class);
- result.addTestSuite(TestExMediaAtom.class);
- result.addTestSuite(TestExObjList.class);
- result.addTestSuite(TestExObjListAtom.class);
- result.addTestSuite(TestExOleObjAtom.class);
- result.addTestSuite(TestExOleObjStg.class);
- result.addTestSuite(TestExVideoContainer.class);
- result.addTestSuite(TestFontCollection.class);
- result.addTestSuite(TestHeadersFootersAtom.class);
- result.addTestSuite(TestHeadersFootersContainer.class);
- result.addTestSuite(TestInteractiveInfo.class);
- result.addTestSuite(TestInteractiveInfoAtom.class);
- result.addTestSuite(TestNotesAtom.class);
- result.addTestSuite(TestRecordContainer.class);
- result.addTestSuite(TestRecordTypes.class);
- result.addTestSuite(TestSlideAtom.class);
- result.addTestSuite(TestSlidePersistAtom.class);
- result.addTestSuite(TestSound.class);
- result.addTestSuite(TestStyleTextPropAtom.class);
- result.addTestSuite(TestTextBytesAtom.class);
- result.addTestSuite(TestTextCharsAtom.class);
- result.addTestSuite(TestTextHeaderAtom.class);
- result.addTestSuite(TestTextRulerAtom.class);
- result.addTestSuite(TestTextSpecInfoAtom.class);
- result.addTestSuite(TestTxInteractiveInfoAtom.class);
- result.addTestSuite(TestTxMasterStyleAtom.class);
- result.addTestSuite(TestUserEditAtom.class);
- return result;
- }
}
package org.apache.poi.hslf.record;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
-import junit.framework.TestCase;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.hslf.HSLFSlideShow;
import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
-import org.apache.poi.POIDataSamples;
+import org.junit.Test;
/**
* Tests that CurrentUserAtom works properly.
*
* @author Nick Burch (nick at torchbox dot com)
*/
-public final class TestCurrentUserAtom extends TestCase {
+public final class TestCurrentUserAtom {
private static POIDataSamples _slTests = POIDataSamples.getSlideShowInstance();
/** Not encrypted */
- private String normalFile;
+ private static final String normalFile = "basic_test_ppt_file.ppt";
/** Encrypted */
- private String encFile;
-
- protected void setUp() throws Exception {
- super.setUp();
+ private static final String encFile = "Password_Protected-hello.ppt";
- normalFile = "basic_test_ppt_file.ppt";
- encFile = "Password_Protected-hello.ppt";
- }
-
- public void testReadNormal() throws Exception {
+ @Test
+ public void readNormal() throws Exception {
POIFSFileSystem fs = new POIFSFileSystem(
_slTests.openResourceAsStream(normalFile)
);
assertEquals(0x2942, cu2.getCurrentEditOffset());
}
- public void testReadEnc() throws Exception {
+ @Test(expected = EncryptedPowerPointFileException.class)
+ public void readEnc() throws Exception {
POIFSFileSystem fs = new POIFSFileSystem(
_slTests.openResourceAsStream(encFile)
);
- try {
- new CurrentUserAtom(fs);
- fail();
- } catch(EncryptedPowerPointFileException e) {
- // Good
- }
+ new CurrentUserAtom(fs);
+ assertTrue(true); // not yet failed
+
+ new HSLFSlideShow(fs);
}
- public void testWriteNormal() throws Exception {
+ @Test
+ public void writeNormal() throws Exception {
// Get raw contents from a known file
POIFSFileSystem fs = new POIFSFileSystem(
_slTests.openResourceAsStream(normalFile)
--- /dev/null
+/* ====================================================================\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to You under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.hslf.record;\r
+\r
+\r
+import static org.junit.Assert.assertArrayEquals;\r
+import static org.junit.Assert.assertEquals;\r
+import static org.junit.Assert.assertTrue;\r
+import static org.junit.Assert.fail;\r
+\r
+import java.io.ByteArrayInputStream;\r
+import java.io.ByteArrayOutputStream;\r
+import java.security.MessageDigest;\r
+\r
+import org.apache.commons.codec.binary.Base64;\r
+import org.apache.poi.POIDataSamples;\r
+import org.apache.poi.hpsf.DocumentSummaryInformation;\r
+import org.apache.poi.hpsf.PropertySet;\r
+import org.apache.poi.hpsf.PropertySetFactory;\r
+import org.apache.poi.hpsf.SummaryInformation;\r
+import org.apache.poi.hslf.HSLFSlideShow;\r
+import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;\r
+import org.apache.poi.hslf.model.Slide;\r
+import org.apache.poi.hslf.usermodel.PictureData;\r
+import org.apache.poi.hslf.usermodel.SlideShow;\r
+import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;\r
+import org.apache.poi.poifs.crypt.CryptoFunctions;\r
+import org.apache.poi.poifs.crypt.EncryptionInfo;\r
+import org.apache.poi.poifs.crypt.HashAlgorithm;\r
+import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader;\r
+import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;\r
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;\r
+import org.junit.Before;\r
+import org.junit.Test;\r
+\r
+/**\r
+ * Tests that DocumentEncryption works properly.\r
+ */\r
+public class TestDocumentEncryption {\r
+ POIDataSamples slTests = POIDataSamples.getSlideShowInstance();\r
+\r
+ @Before\r
+ public void resetPassword() {\r
+ Biff8EncryptionKey.setCurrentUserPassword(null);\r
+ }\r
+ \r
+ @Test\r
+ public void cryptoAPIDecryptionOther() throws Exception {\r
+ Biff8EncryptionKey.setCurrentUserPassword("hello");\r
+ String encPpts[] = {\r
+ "Password_Protected-56-hello.ppt",\r
+ "Password_Protected-hello.ppt",\r
+ "Password_Protected-np-hello.ppt",\r
+ };\r
+ \r
+ for (String pptFile : encPpts) {\r
+ try {\r
+ NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile(pptFile), true);\r
+ HSLFSlideShow hss = new HSLFSlideShow(fs);\r
+ new SlideShow(hss);\r
+ fs.close();\r
+ } catch (EncryptedPowerPointFileException e) {\r
+ fail(pptFile+" can't be decrypted");\r
+ }\r
+ }\r
+ }\r
+\r
+ @Test\r
+ public void cryptoAPIChangeKeySize() throws Exception {\r
+ String pptFile = "cryptoapi-proc2356.ppt";\r
+ Biff8EncryptionKey.setCurrentUserPassword("crypto");\r
+ NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile(pptFile), true);\r
+ HSLFSlideShow hss = new HSLFSlideShow(fs);\r
+ // need to cache data (i.e. read all data) before changing the key size\r
+ PictureData picsExpected[] = hss.getPictures();\r
+ hss.getDocumentSummaryInformation();\r
+ EncryptionInfo ei = hss.getDocumentEncryptionAtom().getEncryptionInfo();\r
+ ((CryptoAPIEncryptionHeader)ei.getHeader()).setKeySize(0x78);\r
+ \r
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();\r
+ hss.write(bos);\r
+ fs.close();\r
+ \r
+ fs = new NPOIFSFileSystem(new ByteArrayInputStream(bos.toByteArray()));\r
+ hss = new HSLFSlideShow(fs);\r
+ PictureData picsActual[] = hss.getPictures();\r
+ fs.close();\r
+ \r
+ assertEquals(picsExpected.length, picsActual.length);\r
+ for (int i=0; i<picsExpected.length; i++) {\r
+ assertArrayEquals(picsExpected[i].getRawData(), picsActual[i].getRawData());\r
+ }\r
+ }\r
+\r
+ @Test\r
+ public void cryptoAPIEncryption() throws Exception {\r
+ /* documents with multiple edits need to be normalized for encryption */\r
+ String pptFile = "57272_corrupted_usereditatom.ppt";\r
+ NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile(pptFile), true);\r
+ HSLFSlideShow hss = new HSLFSlideShow(fs);\r
+ hss.normalizeRecords();\r
+ \r
+ // normalized ppt\r
+ ByteArrayOutputStream expected = new ByteArrayOutputStream();\r
+ hss.write(expected);\r
+ \r
+ // encrypted\r
+ Biff8EncryptionKey.setCurrentUserPassword("hello");\r
+ ByteArrayOutputStream encrypted = new ByteArrayOutputStream();\r
+ hss.write(encrypted);\r
+ fs.close();\r
+\r
+ // decrypted\r
+ ByteArrayInputStream bis = new ByteArrayInputStream(encrypted.toByteArray());\r
+ fs = new NPOIFSFileSystem(bis);\r
+ hss = new HSLFSlideShow(fs);\r
+ Biff8EncryptionKey.setCurrentUserPassword(null);\r
+ ByteArrayOutputStream actual = new ByteArrayOutputStream();\r
+ hss.write(actual);\r
+ fs.close();\r
+ \r
+ assertArrayEquals(expected.toByteArray(), actual.toByteArray());\r
+ } \r
+ \r
+ @Test\r
+ public void cryptoAPIDecryption() throws Exception {\r
+ // taken from a msdn blog:\r
+ // http://blogs.msdn.com/b/openspecification/archive/2009/05/08/dominic-salemno.aspx\r
+ Biff8EncryptionKey.setCurrentUserPassword("crypto");\r
+ NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile("cryptoapi-proc2356.ppt"));\r
+ HSLFSlideShow hss = new HSLFSlideShow(fs);\r
+ SlideShow ss = new SlideShow(hss);\r
+ \r
+ Slide slide = ss.getSlides()[0];\r
+ assertEquals("Dominic Salemno", slide.getTextRuns()[0].getText());\r
+\r
+ String picCmp[][] = {\r
+ {"0","nKsDTKqxTCR8LFkVVWlP9GSTvZ0="},\r
+ {"95163","SuNOR+9V1UVYZIoeD65l3VTaLoc="},\r
+ {"100864","Ql3IGrr4bNq07ZTp5iPg7b+pva8="},\r
+ {"714114","8pdst9NjBGSfWezSZE8+aVhIRe0="},\r
+ {"723752","go6xqW7lvkCtlOO5tYLiMfb4oxw="},\r
+ {"770128","gZUM8YqRNL5kGNfyyYvEEernvCc="},\r
+ {"957958","CNU2iiqUFAnk3TDXsXV1ihH9eRM="}, \r
+ };\r
+ \r
+ MessageDigest md = CryptoFunctions.getMessageDigest(HashAlgorithm.sha1);\r
+ PictureData pd[] = hss.getPictures();\r
+ int i = 0;\r
+ for (PictureData p : pd) {\r
+ byte hash[] = md.digest(p.getData());\r
+ assertEquals(Integer.parseInt(picCmp[i][0]), p.getOffset());\r
+ assertEquals(picCmp[i][1], Base64.encodeBase64String(hash));\r
+ i++;\r
+ }\r
+ \r
+ DocumentEncryptionAtom dea = hss.getDocumentEncryptionAtom();\r
+ \r
+ POIFSFileSystem fs2 = new POIFSFileSystem(dea.getEncryptionInfo().getDecryptor().getDataStream(fs));\r
+ PropertySet ps = PropertySetFactory.create(fs2.getRoot(), SummaryInformation.DEFAULT_STREAM_NAME);\r
+ assertTrue(ps.isSummaryInformation());\r
+ assertEquals("RC4 CryptoAPI Encryption", ps.getProperties()[1].getValue());\r
+ ps = PropertySetFactory.create(fs2.getRoot(), DocumentSummaryInformation.DEFAULT_STREAM_NAME);\r
+ assertTrue(ps.isDocumentSummaryInformation());\r
+ assertEquals("On-screen Show (4:3)", ps.getProperties()[1].getValue());\r
+ }\r
+}\r
package org.apache.poi.hslf.record;
-import junit.framework.TestCase;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import org.junit.Test;
/**
* Tests that DocumentEncryptionAtom works properly.
*
* @author Nick Burch (nick at torchbox dot com)
*/
-public final class TestDocumentEncryptionAtom extends TestCase {
+public final class TestDocumentEncryptionAtom {
// From a real file
private byte[] data_a = new byte[] {
0x0F, 00, 0x14, 0x2F, 0xBE-256, 00, 00, 00,
3, -104, 22, 6, 102, -61, -98, 62, 40, 61, 21
};
- public void testRecordType() {
+ @Test
+ public void recordType() throws IOException {
DocumentEncryptionAtom dea1 = new DocumentEncryptionAtom(data_a, 0, data_a.length);
assertEquals(12052l, dea1.getRecordType());
assertEquals(198, data_b.length);
}
- public void testEncryptionTypeName() {
+ @Test
+ public void encryptionTypeName() throws IOException {
DocumentEncryptionAtom dea1 = new DocumentEncryptionAtom(data_a, 0, data_a.length);
assertEquals("Microsoft Base Cryptographic Provider v1.0", dea1.getEncryptionProviderName());