From: Andreas Beeker Date: Thu, 25 Dec 2014 01:56:29 +0000 (+0000) Subject: - Support for Office Binary Document RC4 CryptoAPI Encryption for HSLF X-Git-Tag: REL_3_12_BETA1~79 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=0839a097e36d53bef39743192e1a1ae7fe8af2cf;p=poi.git - Support for Office Binary Document RC4 CryptoAPI Encryption for HSLF - Support for Office Binary Document RC4 Encryption - use LittleEndian class in LittleEndianInputStream - add normalize method for HSLF, to remove edit history, which is also necessary for encryption support - update PersistDirectoryEntry handling in PersistPtrHolder to recognize groups while serializing - deprecated PersistPtrHolder.getSlideOffsetDataLocationsLookup() - throws now UnsupportedOperationException, as this wasn't used outside the scope of the class and was quite internal logic of PersistPtrHolder git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1647867 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/java/org/apache/poi/POIDocument.java b/src/java/org/apache/poi/POIDocument.java index e61366b451..950e5eb3e4 100644 --- a/src/java/org/apache/poi/POIDocument.java +++ b/src/java/org/apache/poi/POIDocument.java @@ -20,6 +20,7 @@ package org.apache.poi; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.util.List; @@ -28,6 +29,7 @@ import org.apache.poi.hpsf.MutablePropertySet; import org.apache.poi.hpsf.PropertySet; import org.apache.poi.hpsf.PropertySetFactory; import org.apache.poi.hpsf.SummaryInformation; +import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.filesystem.DirectoryEntry; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DocumentInputStream; @@ -163,14 +165,40 @@ public abstract class POIDocument { * @return The value of the given property or null if it wasn't found. */ protected PropertySet getPropertySet(String setName) { + return getPropertySet(setName, null); + } + + /** + * For a given named property entry, either return it or null if + * if it wasn't found + * + * @param setName The property to read + * @param encryptionInfo the encryption descriptor in case of cryptoAPI encryption + * @return The value of the given property or null if it wasn't found. + */ + protected PropertySet getPropertySet(String setName, EncryptionInfo encryptionInfo) { + DirectoryNode dirNode = directory; + + if (encryptionInfo != null) { + try { + InputStream is = encryptionInfo.getDecryptor().getDataStream(directory); + POIFSFileSystem poifs = new POIFSFileSystem(is); + is.close(); + dirNode = poifs.getRoot(); + } catch (Exception e) { + logger.log(POILogger.ERROR, "Error getting encrypted property set with name " + setName, e); + return null; + } + } + //directory can be null when creating new documents - if (directory == null || !directory.hasEntry(setName)) + if (dirNode == null || !dirNode.hasEntry(setName)) return null; DocumentInputStream dis; try { // Find the entry, and get an input stream for it - dis = directory.createDocumentInputStream( directory.getEntry(setName) ); + dis = dirNode.createDocumentInputStream( dirNode.getEntry(setName) ); } catch(IOException ie) { // Oh well, doesn't exist logger.log(POILogger.WARN, "Error getting property set with name " + setName + "\n" + ie); diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java new file mode 100644 index 0000000000..7d695a1eb3 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherInputStream.java @@ -0,0 +1,141 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.poifs.crypt; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; + +import javax.crypto.Cipher; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianInput; +import org.apache.poi.util.LittleEndianInputStream; + +@Internal +public abstract class ChunkedCipherInputStream extends LittleEndianInputStream { + private final int chunkSize; + private final int chunkMask; + private final int chunkBits; + + private int _lastIndex = 0; + private long _pos = 0; + private long _size; + private byte[] _chunk; + private Cipher _cipher; + + public ChunkedCipherInputStream(LittleEndianInput stream, long size, int chunkSize) + throws GeneralSecurityException { + super((InputStream)stream); + _size = size; + this.chunkSize = chunkSize; + chunkMask = chunkSize-1; + chunkBits = Integer.bitCount(chunkMask); + + _cipher = initCipherForBlock(null, 0); + } + + protected abstract Cipher initCipherForBlock(Cipher existing, int block) + throws GeneralSecurityException; + + public int read() throws IOException { + byte[] b = new byte[1]; + if (read(b) == 1) + return b[0]; + return -1; + } + + // do not implement! -> recursion + // public int read(byte[] b) throws IOException; + + public int read(byte[] b, int off, int len) throws IOException { + int total = 0; + + if (available() <= 0) return -1; + + while (len > 0) { + if (_chunk == null) { + try { + _chunk = nextChunk(); + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e.getMessage(), e); + } + } + int count = (int)(chunkSize - (_pos & chunkMask)); + int avail = available(); + if (avail == 0) { + return total; + } + count = Math.min(avail, Math.min(count, len)); + System.arraycopy(_chunk, (int)(_pos & chunkMask), b, off, count); + off += count; + len -= count; + _pos += count; + if ((_pos & chunkMask) == 0) + _chunk = null; + total += count; + } + + return total; + } + + @Override + public long skip(long n) throws IOException { + long start = _pos; + long skip = Math.min(available(), n); + + if ((((_pos + skip) ^ start) & ~chunkMask) != 0) + _chunk = null; + _pos += skip; + return skip; + } + + @Override + public int available() { + return (int)(_size - _pos); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public synchronized void mark(int readlimit) { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized void reset() throws IOException { + throw new UnsupportedOperationException(); + } + + private byte[] nextChunk() throws GeneralSecurityException, IOException { + int index = (int)(_pos >> chunkBits); + initCipherForBlock(_cipher, index); + + if (_lastIndex != index) { + super.skip((index - _lastIndex) << chunkBits); + } + + byte[] block = new byte[Math.min(super.available(), chunkSize)]; + super.read(block, 0, block.length); + _lastIndex = index + 1; + return _cipher.doFinal(block); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java new file mode 100644 index 0000000000..8a2bf00454 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/ChunkedCipherOutputStream.java @@ -0,0 +1,171 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.poifs.crypt; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; + +import javax.crypto.Cipher; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.POIFSWriterEvent; +import org.apache.poi.poifs.filesystem.POIFSWriterListener; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.TempFile; + +@Internal +public abstract class ChunkedCipherOutputStream extends FilterOutputStream { + protected final int chunkSize; + protected final int chunkMask; + protected final int chunkBits; + + private final byte[] _chunk; + private final File fileOut; + private final DirectoryNode dir; + + private long _pos = 0; + private Cipher _cipher; + + public ChunkedCipherOutputStream(DirectoryNode dir, int chunkSize) throws IOException, GeneralSecurityException { + super(null); + this.chunkSize = chunkSize; + chunkMask = chunkSize-1; + chunkBits = Integer.bitCount(chunkMask); + _chunk = new byte[chunkSize]; + + fileOut = TempFile.createTempFile("encrypted_package", "crypt"); + fileOut.deleteOnExit(); + this.out = new FileOutputStream(fileOut); + this.dir = dir; + _cipher = initCipherForBlock(null, 0, false); + } + + protected abstract Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk) + throws GeneralSecurityException; + + protected abstract void calculateChecksum(File fileOut, int oleStreamSize) + throws GeneralSecurityException, IOException; + + protected abstract void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile) + throws IOException, GeneralSecurityException; + + public void write(int b) throws IOException { + write(new byte[]{(byte)b}); + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) + throws IOException { + if (len == 0) return; + + if (len < 0 || b.length < off+len) { + throw new IOException("not enough bytes in your input buffer"); + } + + while (len > 0) { + int posInChunk = (int)(_pos & chunkMask); + int nextLen = Math.min(chunkSize-posInChunk, len); + System.arraycopy(b, off, _chunk, posInChunk, nextLen); + _pos += nextLen; + off += nextLen; + len -= nextLen; + if ((_pos & chunkMask) == 0) { + try { + writeChunk(); + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + } + } + } + + protected void writeChunk() throws IOException, GeneralSecurityException { + int posInChunk = (int)(_pos & chunkMask); + // normally posInChunk is 0, i.e. on the next chunk (-> index-1) + // but if called on close(), posInChunk is somewhere within the chunk data + int index = (int)(_pos >> chunkBits); + boolean lastChunk; + if (posInChunk==0) { + index--; + posInChunk = chunkSize; + lastChunk = false; + } else { + // pad the last chunk + lastChunk = true; + } + + _cipher = initCipherForBlock(_cipher, index, lastChunk); + + int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); + out.write(_chunk, 0, ciLen); + } + + public void close() throws IOException { + try { + writeChunk(); + + super.close(); + + int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE); + calculateChecksum(fileOut, oleStreamSize); + dir.createDocument("EncryptedPackage", oleStreamSize, new EncryptedPackageWriter()); + createEncryptionInfoEntry(dir, fileOut); + } catch (GeneralSecurityException e) { + throw new IOException(e); + } + } + + private class EncryptedPackageWriter implements POIFSWriterListener { + public void processPOIFSWriterEvent(POIFSWriterEvent event) { + try { + OutputStream os = event.getStream(); + byte buf[] = new byte[chunkSize]; + + // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data + // encrypted within the EncryptedData field, not including the size of the StreamSize field. + // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this + // value, depending on the block size of the chosen encryption algorithm + LittleEndian.putLong(buf, 0, _pos); + os.write(buf, 0, LittleEndian.LONG_SIZE); + + FileInputStream fis = new FileInputStream(fileOut); + int readBytes; + while ((readBytes = fis.read(buf)) != -1) { + os.write(buf, 0, readBytes); + } + fis.close(); + + os.close(); + + fileOut.delete(); + } catch (IOException e) { + throw new EncryptedDocumentException(e); + } + } + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/CipherProvider.java b/src/java/org/apache/poi/poifs/crypt/CipherProvider.java index de343a91dc..5ffe1d33ae 100644 --- a/src/java/org/apache/poi/poifs/crypt/CipherProvider.java +++ b/src/java/org/apache/poi/poifs/crypt/CipherProvider.java @@ -20,8 +20,8 @@ package org.apache.poi.poifs.crypt; import org.apache.poi.EncryptedDocumentException; public enum CipherProvider { - rc4("RC4", 1), - aes("AES", 0x18); + rc4("RC4", 1, "Microsoft Base Cryptographic Provider v1.0"), + aes("AES", 0x18, "Microsoft Enhanced RSA and AES Cryptographic Provider"); public static CipherProvider fromEcmaId(int ecmaId) { for (CipherProvider cp : CipherProvider.values()) { @@ -32,8 +32,10 @@ public enum CipherProvider { public final String jceId; public final int ecmaId; - CipherProvider(String jceId, int ecmaId) { + public final String cipherProviderName; + CipherProvider(String jceId, int ecmaId, String cipherProviderName) { this.jceId = jceId; this.ecmaId = ecmaId; + this.cipherProviderName = cipherProviderName; } } \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/Decryptor.java b/src/java/org/apache/poi/poifs/crypt/Decryptor.java index c2d0d5953b..af449290e8 100644 --- a/src/java/org/apache/poi/poifs/crypt/Decryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/Decryptor.java @@ -30,12 +30,12 @@ import org.apache.poi.poifs.filesystem.POIFSFileSystem; public abstract class Decryptor { public static final String DEFAULT_PASSWORD="VelvetSweatshop"; - protected final EncryptionInfo info; + protected final EncryptionInfoBuilder builder; private SecretKey secretKey; private byte[] verifier, integrityHmacKey, integrityHmacValue; - protected Decryptor(EncryptionInfo info) { - this.info = info; + protected Decryptor(EncryptionInfoBuilder builder) { + this.builder = builder; } /** @@ -56,7 +56,7 @@ public abstract class Decryptor { throws GeneralSecurityException; /** - * Returns the length of the encytpted data that can be safely read with + * Returns the length of the encrypted data that can be safely read with * {@link #getDataStream(org.apache.poi.poifs.filesystem.DirectoryNode)}. * Just reading to the end of the input stream is not sufficient because there are * normally padding bytes that must be discarded @@ -120,4 +120,12 @@ public abstract class Decryptor { protected void setIntegrityHmacValue(byte[] integrityHmacValue) { this.integrityHmacValue = integrityHmacValue; } + + protected int getBlockSizeInBytes() { + return builder.getHeader().getBlockSize(); + } + + protected int getKeySizeInBytes() { + return builder.getHeader().getKeySize()/8; + } } \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java index 25f9b01e15..0418befe23 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfo.java @@ -17,15 +17,19 @@ package org.apache.poi.poifs.crypt; import static org.apache.poi.poifs.crypt.EncryptionMode.agile; +import static org.apache.poi.poifs.crypt.EncryptionMode.binaryRC4; +import static org.apache.poi.poifs.crypt.EncryptionMode.cryptoAPI; import static org.apache.poi.poifs.crypt.EncryptionMode.standard; import java.io.IOException; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.filesystem.DirectoryNode; -import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.util.BitField; +import org.apache.poi.util.BitFieldFactory; +import org.apache.poi.util.LittleEndianInput; /** */ @@ -39,6 +43,31 @@ public class EncryptionInfo { private final Decryptor decryptor; private final Encryptor encryptor; + /** + * A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption + * ECMA-376 is used. It MUST be 1 unless flagExternal is 1. If flagExternal is 1, it MUST be 0. + */ + public static BitField flagCryptoAPI = BitFieldFactory.getInstance(0x04); + + /** + * A value that MUST be 0 if document properties are encrypted. + * The encryption of document properties is specified in section 2.3.5.4. + */ + public static BitField flagDocProps = BitFieldFactory.getInstance(0x08); + + /** + * A value that MUST be 1 if extensible encryption is used. If this value is 1, + * the value of every other field in this structure MUST be 0. + */ + public static BitField flagExternal = BitFieldFactory.getInstance(0x10); + + /** + * A value that MUST be 1 if the protected content is an ECMA-376 document + * ECMA-376. If the fAES bit is 1, the fCryptoAPI bit MUST also be 1. + */ + public static BitField flagAES = BitFieldFactory.getInstance(0x20); + + public EncryptionInfo(POIFSFileSystem fs) throws IOException { this(fs.getRoot()); } @@ -48,18 +77,43 @@ public class EncryptionInfo { } public EncryptionInfo(DirectoryNode dir) throws IOException { - DocumentInputStream dis = dir.createDocumentInputStream("EncryptionInfo"); + this(dir.createDocumentInputStream("EncryptionInfo"), false); + } + + public EncryptionInfo(LittleEndianInput dis, boolean isCryptoAPI) throws IOException { + final EncryptionMode encryptionMode; versionMajor = dis.readShort(); versionMinor = dis.readShort(); - encryptionFlags = dis.readInt(); - - EncryptionMode encryptionMode; - if (versionMajor == agile.versionMajor - && versionMinor == agile.versionMinor - && encryptionFlags == agile.encryptionFlags) { + + if (!isCryptoAPI + && versionMajor == binaryRC4.versionMajor + && versionMinor == binaryRC4.versionMinor) { + encryptionMode = binaryRC4; + encryptionFlags = -1; + } else if (!isCryptoAPI + && versionMajor == agile.versionMajor + && versionMinor == agile.versionMinor){ encryptionMode = agile; - } else { + encryptionFlags = dis.readInt(); + } else if (!isCryptoAPI + && 2 <= versionMajor && versionMajor <= 4 + && versionMinor == standard.versionMinor) { encryptionMode = standard; + encryptionFlags = dis.readInt(); + } else if (isCryptoAPI + && 2 <= versionMajor && versionMajor <= 4 + && versionMinor == cryptoAPI.versionMinor) { + encryptionMode = cryptoAPI; + encryptionFlags = dis.readInt(); + } else { + encryptionFlags = dis.readInt(); + throw new EncryptedDocumentException( + "Unknown encryption: version major: "+versionMajor+ + " / version minor: "+versionMinor+ + " / fCrypto: "+flagCryptoAPI.isSet(encryptionFlags)+ + " / fExternal: "+flagExternal.isSet(encryptionFlags)+ + " / fDocProps: "+flagDocProps.isSet(encryptionFlags)+ + " / fAES: "+flagAES.isSet(encryptionFlags)); } EncryptionInfoBuilder eib; @@ -75,22 +129,35 @@ public class EncryptionInfo { decryptor = eib.getDecryptor(); encryptor = eib.getEncryptor(); } - - public EncryptionInfo(POIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException { - this(fs.getRoot(), encryptionMode); - } + + /** + * @deprecated use constructor without fs parameter + */ + @Deprecated + public EncryptionInfo(POIFSFileSystem fs, EncryptionMode encryptionMode) { + this(encryptionMode); + } - public EncryptionInfo(NPOIFSFileSystem fs, EncryptionMode encryptionMode) throws IOException { - this(fs.getRoot(), encryptionMode); - } + /** + * @deprecated use constructor without fs parameter + */ + @Deprecated + public EncryptionInfo(NPOIFSFileSystem fs, EncryptionMode encryptionMode) { + this(encryptionMode); + } - public EncryptionInfo( - DirectoryNode dir - , EncryptionMode encryptionMode - ) throws EncryptedDocumentException { - this(dir, encryptionMode, null, null, -1, -1, null); + /** + * @deprecated use constructor without dir parameter + */ + @Deprecated + public EncryptionInfo(DirectoryNode dir, EncryptionMode encryptionMode) { + this(encryptionMode); } + /** + * @deprecated use constructor without fs parameter + */ + @Deprecated public EncryptionInfo( POIFSFileSystem fs , EncryptionMode encryptionMode @@ -99,10 +166,14 @@ public class EncryptionInfo { , int keyBits , int blockSize , ChainingMode chainingMode - ) throws EncryptedDocumentException { - this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); + ) { + this(encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); } + /** + * @deprecated use constructor without fs parameter + */ + @Deprecated public EncryptionInfo( NPOIFSFileSystem fs , EncryptionMode encryptionMode @@ -111,10 +182,14 @@ public class EncryptionInfo { , int keyBits , int blockSize , ChainingMode chainingMode - ) throws EncryptedDocumentException { - this(fs.getRoot(), encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); + ) { + this(encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); } + /** + * @deprecated use constructor without dir parameter + */ + @Deprecated public EncryptionInfo( DirectoryNode dir , EncryptionMode encryptionMode @@ -123,7 +198,36 @@ public class EncryptionInfo { , int keyBits , int blockSize , ChainingMode chainingMode - ) throws EncryptedDocumentException { + ) { + this(encryptionMode, cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); + } + + public EncryptionInfo(EncryptionMode encryptionMode) { + this(encryptionMode, null, null, -1, -1, null); + } + + /** + * Constructs an EncryptionInfo from scratch + * + * @param encryptionMode see {@link EncryptionMode} for values, {@link EncryptionMode#cryptoAPI} is for + * internal use only, as it's record based + * @param cipherAlgorithm + * @param hashAlgorithm + * @param keyBits + * @param blockSize + * @param chainingMode + * + * @throws EncryptedDocumentException if the given parameters mismatch, e.g. only certain combinations + * of keyBits, blockSize are allowed for a given {@link CipherAlgorithm} + */ + public EncryptionInfo( + EncryptionMode encryptionMode + , CipherAlgorithm cipherAlgorithm + , HashAlgorithm hashAlgorithm + , int keyBits + , int blockSize + , ChainingMode chainingMode + ) { versionMajor = encryptionMode.versionMajor; versionMinor = encryptionMode.versionMinor; encryptionFlags = encryptionMode.encryptionFlags; diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java index 0c31fc8fdc..e36d44da9e 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionInfoBuilder.java @@ -18,13 +18,36 @@ package org.apache.poi.poifs.crypt; import java.io.IOException; -import org.apache.poi.poifs.filesystem.DocumentInputStream; +import org.apache.poi.util.LittleEndianInput; public interface EncryptionInfoBuilder { - void initialize(EncryptionInfo ei, DocumentInputStream dis) throws IOException; + /** + * initialize the builder from a stream + */ + void initialize(EncryptionInfo ei, LittleEndianInput dis) throws IOException; + + /** + * initialize the builder from scratch + */ void initialize(EncryptionInfo ei, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode); + + /** + * @return the header data + */ EncryptionHeader getHeader(); + + /** + * @return the verifier data + */ EncryptionVerifier getVerifier(); + + /** + * @return the decryptor + */ Decryptor getDecryptor(); + + /** + * @return the encryptor + */ Encryptor getEncryptor(); } diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java index 4d9114573f..86f4b8508a 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionMode.java @@ -17,9 +17,24 @@ package org.apache.poi.poifs.crypt; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; + +/** + * Office supports various encryption modes. + * The encryption is either based on the whole container ({@link #agile}, {@link #standard} or {@link #binaryRC4}) + * or record based ({@link #cryptoAPI}). The record based encryption can't be accessed directly, but will be + * invoked by using the {@link Biff8EncryptionKey#setCurrentUserPassword(String)} before saving the document. + */ public enum EncryptionMode { - standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24) - , agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40); + /* @see 2.3.6 Office Binary Document RC4 Encryption */ + binaryRC4("org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionInfoBuilder", 1, 1, 0x0), + /* @see 2.3.5 Office Binary Document RC4 CryptoAPI Encryption */ + cryptoAPI("org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionInfoBuilder", 4, 2, 0x04), + /* @see 2.3.4.5 \EncryptionInfo Stream (Standard Encryption) */ + standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24), + /* @see 2.3.4.10 \EncryptionInfo Stream (Agile Encryption) */ + agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40) + ; public final String builder; public final int versionMajor; diff --git a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java index ecb90e08e2..9dafc11bf7 100644 --- a/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java +++ b/src/java/org/apache/poi/poifs/crypt/EncryptionVerifier.java @@ -41,6 +41,7 @@ public abstract class EncryptionVerifier { * The method name is misleading - you'll get the encrypted verifier, not the plain verifier * @deprecated use getEncryptedVerifier() */ + @Deprecated public byte[] getVerifier() { return encryptedVerifier; } @@ -53,6 +54,7 @@ public abstract class EncryptionVerifier { * The method name is misleading - you'll get the encrypted verifier hash, not the plain verifier hash * @deprecated use getEnryptedVerifierHash */ + @Deprecated public byte[] getVerifierHash() { return encryptedVerifierHash; } @@ -76,6 +78,7 @@ public abstract class EncryptionVerifier { /** * @deprecated use getCipherAlgorithm().jceId */ + @Deprecated public String getAlgorithmName() { return cipherAlgorithm.jceId; } diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java new file mode 100644 index 0000000000..89b2b1f766 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Decryptor.java @@ -0,0 +1,131 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.binaryrc4; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.util.Arrays; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.*; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.DocumentInputStream; +import org.apache.poi.util.LittleEndian; + +public class BinaryRC4Decryptor extends Decryptor { + private long _length = -1L; + + private class BinaryRC4CipherInputStream extends ChunkedCipherInputStream { + + protected Cipher initCipherForBlock(Cipher existing, int block) + throws GeneralSecurityException { + return BinaryRC4Decryptor.initCipherForBlock(existing, block, builder, getSecretKey(), Cipher.DECRYPT_MODE); + } + + public BinaryRC4CipherInputStream(DocumentInputStream stream, long size) + throws GeneralSecurityException { + super(stream, size, 512); + } + } + + protected BinaryRC4Decryptor(BinaryRC4EncryptionInfoBuilder builder) { + super(builder); + } + + public boolean verifyPassword(String password) { + EncryptionVerifier ver = builder.getVerifier(); + SecretKey skey = generateSecretKey(password, ver); + try { + Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE); + byte encryptedVerifier[] = ver.getEncryptedVerifier(); + byte verifier[] = new byte[encryptedVerifier.length]; + cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier); + setVerifier(verifier); + byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash(); + byte verifierHash[] = cipher.doFinal(encryptedVerifierHash); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); + byte calcVerifierHash[] = hashAlg.digest(verifier); + if (Arrays.equals(calcVerifierHash, verifierHash)) { + setSecretKey(skey); + return true; + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e); + } + return false; + } + + protected static Cipher initCipherForBlock(Cipher cipher, int block, + EncryptionInfoBuilder builder, SecretKey skey, int encryptMode) + throws GeneralSecurityException { + EncryptionVerifier ver = builder.getVerifier(); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + byte blockKey[] = new byte[4]; + LittleEndian.putUInt(blockKey, 0, block); + byte encKey[] = CryptoFunctions.generateKey(skey.getEncoded(), hashAlgo, blockKey, 16); + SecretKey key = new SecretKeySpec(encKey, skey.getAlgorithm()); + if (cipher == null) { + EncryptionHeader em = builder.getHeader(); + cipher = CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), null, null, encryptMode); + } else { + cipher.init(encryptMode, key); + } + return cipher; + } + + protected static SecretKey generateSecretKey(String password, + EncryptionVerifier ver) { + if (password.length() > 255) + password = password.substring(0, 255); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); + byte hash[] = hashAlg.digest(CryptoFunctions.getUtf16LeString(password)); + byte salt[] = ver.getSalt(); + hashAlg.reset(); + for (int i = 0; i < 16; i++) { + hashAlg.update(hash, 0, 5); + hashAlg.update(salt); + } + + hash = new byte[5]; + System.arraycopy(hashAlg.digest(), 0, hash, 0, 5); + SecretKey skey = new SecretKeySpec(hash, ver.getCipherAlgorithm().jceId); + return skey; + } + + public InputStream getDataStream(DirectoryNode dir) throws IOException, + GeneralSecurityException { + DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage"); + _length = dis.readLong(); + BinaryRC4CipherInputStream cipherStream = new BinaryRC4CipherInputStream(dis, _length); + return cipherStream; + } + + public long getLength() { + if (_length == -1L) { + throw new IllegalStateException("Decryptor.getDataStream() was not called"); + } + + return _length; + } +} \ No newline at end of file diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java new file mode 100644 index 0000000000..1b811a1031 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionHeader.java @@ -0,0 +1,44 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.binaryrc4; + +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.CipherProvider; +import org.apache.poi.poifs.crypt.EncryptionHeader; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; + +public class BinaryRC4EncryptionHeader extends EncryptionHeader implements + EncryptionRecord { + + protected BinaryRC4EncryptionHeader() { + setCipherAlgorithm(CipherAlgorithm.rc4); + setKeySize(40); + setBlockSize(-1); + setCipherProvider(CipherProvider.rc4); + setHashAlgorithm(HashAlgorithm.md5); + setSizeExtra(0); + setFlags(0); + setCspName(""); + setChainingMode(null); + } + + public void write(LittleEndianByteArrayOutputStream littleendianbytearrayoutputstream) { + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java new file mode 100644 index 0000000000..10bf58d83b --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionInfoBuilder.java @@ -0,0 +1,77 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.binaryrc4; + +import java.io.IOException; +import org.apache.poi.poifs.crypt.*; +import org.apache.poi.util.LittleEndianInput; + +public class BinaryRC4EncryptionInfoBuilder implements EncryptionInfoBuilder { + + EncryptionInfo info; + BinaryRC4EncryptionHeader header; + BinaryRC4EncryptionVerifier verifier; + BinaryRC4Decryptor decryptor; + BinaryRC4Encryptor encryptor; + + public BinaryRC4EncryptionInfoBuilder() { + } + + public void initialize(EncryptionInfo info, LittleEndianInput dis) + throws IOException { + this.info = info; + int vMajor = info.getVersionMajor(); + int vMinor = info.getVersionMinor(); + assert (vMajor == 1 && vMinor == 1); + + header = new BinaryRC4EncryptionHeader(); + verifier = new BinaryRC4EncryptionVerifier(dis); + decryptor = new BinaryRC4Decryptor(this); + encryptor = new BinaryRC4Encryptor(this); + } + + public void initialize(EncryptionInfo info, + CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, + int keyBits, int blockSize, ChainingMode chainingMode) { + this.info = info; + header = new BinaryRC4EncryptionHeader(); + verifier = new BinaryRC4EncryptionVerifier(); + decryptor = new BinaryRC4Decryptor(this); + encryptor = new BinaryRC4Encryptor(this); + } + + public BinaryRC4EncryptionHeader getHeader() { + return header; + } + + public BinaryRC4EncryptionVerifier getVerifier() { + return verifier; + } + + public BinaryRC4Decryptor getDecryptor() { + return decryptor; + } + + public BinaryRC4Encryptor getEncryptor() { + return encryptor; + } + + public EncryptionInfo getEncryptionInfo() { + return info; + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java new file mode 100644 index 0000000000..86cf4ac184 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4EncryptionVerifier.java @@ -0,0 +1,81 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.binaryrc4; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.*; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; +import org.apache.poi.util.LittleEndianInput; + +public class BinaryRC4EncryptionVerifier extends EncryptionVerifier implements EncryptionRecord { + + protected BinaryRC4EncryptionVerifier() { + setSpinCount(-1); + setCipherAlgorithm(CipherAlgorithm.rc4); + setChainingMode(null); + setEncryptedKey(null); + setHashAlgorithm(HashAlgorithm.md5); + } + + protected BinaryRC4EncryptionVerifier(LittleEndianInput is) { + byte salt[] = new byte[16]; + is.readFully(salt); + setSalt(salt); + byte encryptedVerifier[] = new byte[16]; + is.readFully(encryptedVerifier); + setEncryptedVerifier(encryptedVerifier); + byte encryptedVerifierHash[] = new byte[16]; + is.readFully(encryptedVerifierHash); + setEncryptedVerifierHash(encryptedVerifierHash); + setSpinCount(-1); + setCipherAlgorithm(CipherAlgorithm.rc4); + setChainingMode(null); + setEncryptedKey(null); + setHashAlgorithm(HashAlgorithm.md5); + } + + protected void setSalt(byte salt[]) { + if (salt == null || salt.length != 16) { + throw new EncryptedDocumentException("invalid verifier salt"); + } + + super.setSalt(salt); + } + + protected void setEncryptedVerifier(byte encryptedVerifier[]) { + super.setEncryptedVerifier(encryptedVerifier); + } + + protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) { + super.setEncryptedVerifierHash(encryptedVerifierHash); + } + + public void write(LittleEndianByteArrayOutputStream bos) { + byte salt[] = getSalt(); + assert (salt.length == 16); + bos.write(salt); + byte encryptedVerifier[] = getEncryptedVerifier(); + assert (encryptedVerifier.length == 16); + bos.write(encryptedVerifier); + byte encryptedVerifierHash[] = getEncryptedVerifierHash(); + assert (encryptedVerifierHash.length == 16); + bos.write(encryptedVerifierHash); + } + +} diff --git a/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java new file mode 100644 index 0000000000..2cf2d93347 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/binaryrc4/BinaryRC4Encryptor.java @@ -0,0 +1,127 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.binaryrc4; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Random; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.DataSpaceMapUtils; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.Encryptor; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; + +public class BinaryRC4Encryptor extends Encryptor { + + private final BinaryRC4EncryptionInfoBuilder builder; + + protected class BinaryRC4CipherOutputStream extends ChunkedCipherOutputStream { + + protected Cipher initCipherForBlock(Cipher cipher, int block, boolean lastChunk) + throws GeneralSecurityException { + return BinaryRC4Decryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE); + } + + protected void calculateChecksum(File file, int i) { + } + + protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile) + throws IOException, GeneralSecurityException { + BinaryRC4Encryptor.this.createEncryptionInfoEntry(dir); + } + + public BinaryRC4CipherOutputStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + super(dir, 512); + } + } + + protected BinaryRC4Encryptor(BinaryRC4EncryptionInfoBuilder builder) { + this.builder = builder; + } + + public void confirmPassword(String password) { + Random r = new SecureRandom(); + byte salt[] = new byte[16]; + byte verifier[] = new byte[16]; + r.nextBytes(salt); + r.nextBytes(verifier); + confirmPassword(password, null, null, verifier, salt, null); + } + + public void confirmPassword(String password, byte keySpec[], + byte keySalt[], byte verifier[], byte verifierSalt[], + byte integritySalt[]) { + BinaryRC4EncryptionVerifier ver = builder.getVerifier(); + ver.setSalt(verifierSalt); + SecretKey skey = BinaryRC4Decryptor.generateSecretKey(password, ver); + setSecretKey(skey); + try { + Cipher cipher = BinaryRC4Decryptor.initCipherForBlock(null, 0, builder, skey, Cipher.ENCRYPT_MODE); + byte encryptedVerifier[] = new byte[16]; + cipher.update(verifier, 0, 16, encryptedVerifier); + ver.setEncryptedVerifier(encryptedVerifier); + org.apache.poi.poifs.crypt.HashAlgorithm hashAlgo = ver + .getHashAlgorithm(); + MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); + byte calcVerifierHash[] = hashAlg.digest(verifier); + byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash); + ver.setEncryptedVerifierHash(encryptedVerifierHash); + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException("Password confirmation failed", e); + } + } + + public OutputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + OutputStream countStream = new BinaryRC4CipherOutputStream(dir); + return countStream; + } + + protected int getKeySizeInBytes() { + return builder.getHeader().getKeySize() / 8; + } + + protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { + DataSpaceMapUtils.addDefaultDataSpace(dir); + final EncryptionInfo info = builder.getEncryptionInfo(); + final BinaryRC4EncryptionHeader header = builder.getHeader(); + final BinaryRC4EncryptionVerifier verifier = builder.getVerifier(); + EncryptionRecord er = new EncryptionRecord() { + public void write(LittleEndianByteArrayOutputStream bos) { + bos.writeShort(info.getVersionMajor()); + bos.writeShort(info.getVersionMinor()); + header.write(bos); + verifier.write(bos); + } + }; + DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java new file mode 100644 index 0000000000..7042adffec --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIDecryptor.java @@ -0,0 +1,259 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.cryptoapi; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.Decryptor; +import org.apache.poi.poifs.crypt.EncryptionHeader; +import org.apache.poi.poifs.crypt.EncryptionInfoBuilder; +import org.apache.poi.poifs.crypt.EncryptionVerifier; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.DocumentInputStream; +import org.apache.poi.poifs.filesystem.DocumentNode; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.util.BitField; +import org.apache.poi.util.BitFieldFactory; +import org.apache.poi.util.BoundedInputStream; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianInputStream; + +public class CryptoAPIDecryptor extends Decryptor { + + private long _length; + + private class SeekableByteArrayInputStream extends ByteArrayInputStream { + Cipher cipher; + byte oneByte[] = { 0 }; + + public void seek(int pos) { + if (pos > count) { + throw new ArrayIndexOutOfBoundsException(pos); + } + + this.pos = pos; + mark = pos; + } + + public void setBlock(int block) throws GeneralSecurityException { + cipher = initCipherForBlock(cipher, block); + } + + public synchronized int read() { + int ch = super.read(); + if (ch == -1) return -1; + oneByte[0] = (byte) ch; + try { + cipher.update(oneByte, 0, 1, oneByte); + } catch (ShortBufferException e) { + throw new EncryptedDocumentException(e); + } + return oneByte[0]; + } + + public synchronized int read(byte b[], int off, int len) { + int readLen = super.read(b, off, len); + if (readLen ==-1) return -1; + try { + cipher.update(b, off, readLen, b, off); + } catch (ShortBufferException e) { + throw new EncryptedDocumentException(e); + } + return readLen; + } + + public SeekableByteArrayInputStream(byte buf[]) + throws GeneralSecurityException { + super(buf); + cipher = initCipherForBlock(null, 0); + } + } + + static class StreamDescriptorEntry { + static BitField flagStream = BitFieldFactory.getInstance(1); + + int streamOffset; + int streamSize; + int block; + int flags; + int reserved2; + String streamName; + } + + protected CryptoAPIDecryptor(CryptoAPIEncryptionInfoBuilder builder) { + super(builder); + _length = -1L; + } + + public boolean verifyPassword(String password) { + EncryptionVerifier ver = builder.getVerifier(); + SecretKey skey = generateSecretKey(password, ver); + try { + Cipher cipher = initCipherForBlock(null, 0, builder, skey, Cipher.DECRYPT_MODE); + byte encryptedVerifier[] = ver.getEncryptedVerifier(); + byte verifier[] = new byte[encryptedVerifier.length]; + cipher.update(encryptedVerifier, 0, encryptedVerifier.length, verifier); + setVerifier(verifier); + byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash(); + byte verifierHash[] = cipher.doFinal(encryptedVerifierHash); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); + byte calcVerifierHash[] = hashAlg.digest(verifier); + if (Arrays.equals(calcVerifierHash, verifierHash)) { + setSecretKey(skey); + return true; + } + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException(e); + } + return false; + } + + /** + * Initializes a cipher object for a given block index for decryption + * + * @param cipher may be null, otherwise the given instance is reset to the new block index + * @param block the block index, e.g. the persist/slide id (hslf) + * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher + * @throws GeneralSecurityException + */ + public Cipher initCipherForBlock(Cipher cipher, int block) + throws GeneralSecurityException { + return initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.DECRYPT_MODE); + } + + protected static Cipher initCipherForBlock(Cipher cipher, int block, + EncryptionInfoBuilder builder, SecretKey skey, int encryptMode) + throws GeneralSecurityException { + EncryptionVerifier ver = builder.getVerifier(); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + byte blockKey[] = new byte[4]; + LittleEndian.putUInt(blockKey, 0, block); + MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); + hashAlg.update(skey.getEncoded()); + byte encKey[] = hashAlg.digest(blockKey); + EncryptionHeader header = builder.getHeader(); + int keyBits = header.getKeySize(); + encKey = CryptoFunctions.getBlock0(encKey, keyBits / 8); + if (keyBits == 40) { + encKey = CryptoFunctions.getBlock0(encKey, 16); + } + SecretKey key = new SecretKeySpec(encKey, skey.getAlgorithm()); + if (cipher == null) { + cipher = CryptoFunctions.getCipher(key, header.getCipherAlgorithm(), null, null, encryptMode); + } else { + cipher.init(encryptMode, key); + } + return cipher; + } + + protected static SecretKey generateSecretKey(String password, EncryptionVerifier ver) { + if (password.length() > 255) { + password = password.substring(0, 255); + } + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); + hashAlg.update(ver.getSalt()); + byte hash[] = hashAlg.digest(CryptoFunctions.getUtf16LeString(password)); + SecretKey skey = new SecretKeySpec(hash, ver.getCipherAlgorithm().jceId); + return skey; + } + + /** + * Decrypt the Document-/SummaryInformation and other optionally streams. + * Opposed to other crypto modes, cryptoapi is record based and can't be used + * to stream-decrypt a whole file + * + * @see 2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream + */ + @SuppressWarnings("unused") + public InputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + POIFSFileSystem fsOut = new POIFSFileSystem(); + DocumentNode es = (DocumentNode) dir.getEntry("EncryptedSummary"); + DocumentInputStream dis = dir.createDocumentInputStream(es); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + IOUtils.copy(dis, bos); + dis.close(); + SeekableByteArrayInputStream sbis = new SeekableByteArrayInputStream(bos.toByteArray()); + LittleEndianInputStream leis = new LittleEndianInputStream(sbis); + int streamDescriptorArrayOffset = (int) leis.readUInt(); + int streamDescriptorArraySize = (int) leis.readUInt(); + sbis.skip(streamDescriptorArrayOffset - 8); + sbis.setBlock(0); + int encryptedStreamDescriptorCount = (int) leis.readUInt(); + StreamDescriptorEntry entries[] = new StreamDescriptorEntry[encryptedStreamDescriptorCount]; + for (int i = 0; i < encryptedStreamDescriptorCount; i++) { + StreamDescriptorEntry entry = new StreamDescriptorEntry(); + entries[i] = entry; + entry.streamOffset = (int) leis.readUInt(); + entry.streamSize = (int) leis.readUInt(); + entry.block = leis.readUShort(); + int nameSize = leis.readUByte(); + entry.flags = leis.readUByte(); + boolean isStream = StreamDescriptorEntry.flagStream.isSet(entry.flags); + entry.reserved2 = leis.readInt(); + byte nameBuf[] = new byte[nameSize * 2]; + leis.read(nameBuf); + entry.streamName = new String(nameBuf, Charset.forName("UTF-16LE")); + leis.readShort(); + assert(entry.streamName.length() == nameSize); + } + + for (StreamDescriptorEntry entry : entries) { + sbis.seek(entry.streamOffset); + sbis.setBlock(entry.block); + InputStream is = new BoundedInputStream(sbis, entry.streamSize); + fsOut.createDocument(is, entry.streamName); + } + + leis.close(); + sbis = null; + bos.reset(); + fsOut.writeFilesystem(bos); + _length = bos.size(); + ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); + return bis; + } + + /** + * @return the length of the stream returned by {@link #getDataStream(DirectoryNode)} + */ + public long getLength() { + if (_length == -1L) { + throw new IllegalStateException("Decryptor.getDataStream() was not called"); + } + return _length; + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java new file mode 100644 index 0000000000..151b6588ae --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionHeader.java @@ -0,0 +1,62 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.cryptoapi; + +import java.io.IOException; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.CipherProvider; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.standard.StandardEncryptionHeader; +import org.apache.poi.util.LittleEndianInput; + +public class CryptoAPIEncryptionHeader extends StandardEncryptionHeader { + + public CryptoAPIEncryptionHeader(LittleEndianInput is) throws IOException { + super(is); + } + + protected CryptoAPIEncryptionHeader(CipherAlgorithm cipherAlgorithm, + HashAlgorithm hashAlgorithm, int keyBits, int blockSize, + ChainingMode chainingMode) { + super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); + } + + public void setKeySize(int keyBits) { + // Microsoft Base Cryptographic Provider is limited up to 40 bits + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa375599(v=vs.85).aspx + boolean found = false; + for (int size : getCipherAlgorithm().allowedKeySize) { + if (size == keyBits) { + found = true; + break; + } + } + if (!found) { + throw new EncryptedDocumentException("invalid keysize "+keyBits+" for cipher algorithm "+getCipherAlgorithm()); + } + super.setKeySize(keyBits); + if (keyBits > 40) { + setCspName("Microsoft Enhanced Cryptographic Provider v1.0"); + } else { + setCspName(CipherProvider.rc4.cipherProviderName); + } + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java new file mode 100644 index 0000000000..2a8a872642 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionInfoBuilder.java @@ -0,0 +1,86 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.cryptoapi; + +import java.io.IOException; + +import org.apache.poi.poifs.crypt.*; +import org.apache.poi.util.LittleEndianInput; + +public class CryptoAPIEncryptionInfoBuilder implements EncryptionInfoBuilder { + EncryptionInfo info; + CryptoAPIEncryptionHeader header; + CryptoAPIEncryptionVerifier verifier; + CryptoAPIDecryptor decryptor; + CryptoAPIEncryptor encryptor; + + public CryptoAPIEncryptionInfoBuilder() { + } + + /** + * initialize the builder from a stream + */ + @SuppressWarnings("unused") + public void initialize(EncryptionInfo info, LittleEndianInput dis) + throws IOException { + this.info = info; + int hSize = dis.readInt(); + header = new CryptoAPIEncryptionHeader(dis); + verifier = new CryptoAPIEncryptionVerifier(dis, header); + decryptor = new CryptoAPIDecryptor(this); + encryptor = new CryptoAPIEncryptor(this); + } + + /** + * initialize the builder from scratch + */ + public void initialize(EncryptionInfo info, + CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, + int keyBits, int blockSize, ChainingMode chainingMode) { + this.info = info; + if (cipherAlgorithm == null) cipherAlgorithm = CipherAlgorithm.rc4; + if (hashAlgorithm == null) hashAlgorithm = HashAlgorithm.sha1; + if (keyBits == -1) keyBits = 0x28; + assert(cipherAlgorithm == CipherAlgorithm.rc4 && hashAlgorithm == HashAlgorithm.sha1); + + header = new CryptoAPIEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); + verifier = new CryptoAPIEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); + decryptor = new CryptoAPIDecryptor(this); + encryptor = new CryptoAPIEncryptor(this); + } + + public CryptoAPIEncryptionHeader getHeader() { + return header; + } + + public CryptoAPIEncryptionVerifier getVerifier() { + return verifier; + } + + public CryptoAPIDecryptor getDecryptor() { + return decryptor; + } + + public CryptoAPIEncryptor getEncryptor() { + return encryptor; + } + + public EncryptionInfo getEncryptionInfo() { + return info; + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java new file mode 100644 index 0000000000..160d1f9f91 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptionVerifier.java @@ -0,0 +1,50 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.cryptoapi; + +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.standard.StandardEncryptionVerifier; +import org.apache.poi.util.LittleEndianInput; + +public class CryptoAPIEncryptionVerifier extends StandardEncryptionVerifier { + + protected CryptoAPIEncryptionVerifier(LittleEndianInput is, + CryptoAPIEncryptionHeader header) { + super(is, header); + } + + protected CryptoAPIEncryptionVerifier(CipherAlgorithm cipherAlgorithm, + HashAlgorithm hashAlgorithm, int keyBits, int blockSize, + ChainingMode chainingMode) { + super(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode); + } + + protected void setSalt(byte salt[]) { + super.setSalt(salt); + } + + protected void setEncryptedVerifier(byte encryptedVerifier[]) { + super.setEncryptedVerifier(encryptedVerifier); + } + + protected void setEncryptedVerifierHash(byte encryptedVerifierHash[]) { + super.setEncryptedVerifierHash(encryptedVerifierHash); + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java new file mode 100644 index 0000000000..d237f50634 --- /dev/null +++ b/src/java/org/apache/poi/poifs/crypt/cryptoapi/CryptoAPIEncryptor.java @@ -0,0 +1,255 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.cryptoapi; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.hpsf.DocumentSummaryInformation; +import org.apache.poi.hpsf.PropertySetFactory; +import org.apache.poi.hpsf.SummaryInformation; +import org.apache.poi.hpsf.WritingNotSupportedException; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.DataSpaceMapUtils; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.Encryptor; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIDecryptor.StreamDescriptorEntry; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.DocumentInputStream; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianByteArrayOutputStream; + +public class CryptoAPIEncryptor extends Encryptor { + private final CryptoAPIEncryptionInfoBuilder builder; + + protected CryptoAPIEncryptor(CryptoAPIEncryptionInfoBuilder builder) { + this.builder = builder; + } + + public void confirmPassword(String password) { + Random r = new SecureRandom(); + byte salt[] = new byte[16]; + byte verifier[] = new byte[16]; + r.nextBytes(salt); + r.nextBytes(verifier); + confirmPassword(password, null, null, verifier, salt, null); + } + + public void confirmPassword(String password, byte keySpec[], + byte keySalt[], byte verifier[], byte verifierSalt[], + byte integritySalt[]) { + assert(verifier != null && verifierSalt != null); + CryptoAPIEncryptionVerifier ver = builder.getVerifier(); + ver.setSalt(verifierSalt); + SecretKey skey = CryptoAPIDecryptor.generateSecretKey(password, ver); + setSecretKey(skey); + try { + Cipher cipher = initCipherForBlock(null, 0); + byte encryptedVerifier[] = new byte[verifier.length]; + cipher.update(verifier, 0, verifier.length, encryptedVerifier); + ver.setEncryptedVerifier(encryptedVerifier); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + MessageDigest hashAlg = CryptoFunctions.getMessageDigest(hashAlgo); + byte calcVerifierHash[] = hashAlg.digest(verifier); + byte encryptedVerifierHash[] = cipher.doFinal(calcVerifierHash); + ver.setEncryptedVerifierHash(encryptedVerifierHash); + } catch (GeneralSecurityException e) { + throw new EncryptedDocumentException("Password confirmation failed", e); + } + } + + /** + * Initializes a cipher object for a given block index for encryption + * + * @param cipher may be null, otherwise the given instance is reset to the new block index + * @param block the block index, e.g. the persist/slide id (hslf) + * @return a new cipher object, if cipher was null, otherwise the reinitialized cipher + * @throws GeneralSecurityException + */ + public Cipher initCipherForBlock(Cipher cipher, int block) + throws GeneralSecurityException { + return CryptoAPIDecryptor.initCipherForBlock(cipher, block, builder, getSecretKey(), Cipher.ENCRYPT_MODE); + } + + /** + * Encrypt the Document-/SummaryInformation and other optionally streams. + * Opposed to other crypto modes, cryptoapi is record based and can't be used + * to stream-encrypt a whole file + * + * @see 2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream + */ + public OutputStream getDataStream(DirectoryNode dir) + throws IOException, GeneralSecurityException { + CipherByteArrayOutputStream bos = new CipherByteArrayOutputStream(); + byte buf[] = new byte[8]; + + bos.write(buf, 0, 8); // skip header + String entryNames[] = { + SummaryInformation.DEFAULT_STREAM_NAME, + DocumentSummaryInformation.DEFAULT_STREAM_NAME + }; + + List descList = new ArrayList(); + + int block = 0; + for (String entryName : entryNames) { + if (!dir.hasEntry(entryName)) continue; + StreamDescriptorEntry descEntry = new StreamDescriptorEntry(); + descEntry.block = block; + descEntry.streamOffset = bos.size(); + descEntry.streamName = entryName; + descEntry.flags = StreamDescriptorEntry.flagStream.setValue(0, 1); + descEntry.reserved2 = 0; + + bos.setBlock(block); + DocumentInputStream dis = dir.createDocumentInputStream(entryName); + IOUtils.copy(dis, bos); + dis.close(); + + descEntry.streamSize = bos.size() - descEntry.streamOffset; + descList.add(descEntry); + + dir.getEntry(entryName).delete(); + + block++; + } + + int streamDescriptorArrayOffset = bos.size(); + + bos.setBlock(0); + LittleEndian.putUInt(buf, 0, descList.size()); + bos.write(buf, 0, 4); + + for (StreamDescriptorEntry sde : descList) { + LittleEndian.putUInt(buf, 0, sde.streamOffset); + bos.write(buf, 0, 4); + LittleEndian.putUInt(buf, 0, sde.streamSize); + bos.write(buf, 0, 4); + LittleEndian.putUShort(buf, 0, sde.block); + bos.write(buf, 0, 2); + LittleEndian.putUByte(buf, 0, (short)sde.streamName.length()); + bos.write(buf, 0, 1); + LittleEndian.putUByte(buf, 0, (short)sde.flags); + bos.write(buf, 0, 1); + LittleEndian.putUInt(buf, 0, sde.reserved2); + bos.write(buf, 0, 4); + byte nameBytes[] = sde.streamName.getBytes(Charset.forName("UTF-16LE")); + bos.write(nameBytes, 0, nameBytes.length); + LittleEndian.putShort(buf, 0, (short)0); // null-termination + bos.write(buf, 0, 2); + } + + int savedSize = bos.size(); + int streamDescriptorArraySize = savedSize - streamDescriptorArrayOffset; + LittleEndian.putUInt(buf, 0, streamDescriptorArrayOffset); + LittleEndian.putUInt(buf, 4, streamDescriptorArraySize); + + bos.reset(); + bos.setBlock(0); + bos.write(buf, 0, 8); + bos.setSize(savedSize); + + dir.createDocument("EncryptedSummary", new ByteArrayInputStream(bos.getBuf(), 0, savedSize)); + DocumentSummaryInformation dsi = PropertySetFactory.newDocumentSummaryInformation(); + + try { + dsi.write(dir, DocumentSummaryInformation.DEFAULT_STREAM_NAME); + } catch (WritingNotSupportedException e) { + throw new IOException(e); + } + + return bos; + } + + protected int getKeySizeInBytes() { + return builder.getHeader().getKeySize() / 8; + } + + protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { + DataSpaceMapUtils.addDefaultDataSpace(dir); + final EncryptionInfo info = builder.getEncryptionInfo(); + final CryptoAPIEncryptionHeader header = builder.getHeader(); + final CryptoAPIEncryptionVerifier verifier = builder.getVerifier(); + EncryptionRecord er = new EncryptionRecord() { + public void write(LittleEndianByteArrayOutputStream bos) { + bos.writeShort(info.getVersionMajor()); + bos.writeShort(info.getVersionMinor()); + header.write(bos); + verifier.write(bos); + } + }; + DataSpaceMapUtils.createEncryptionEntry(dir, "EncryptionInfo", er); + } + + private class CipherByteArrayOutputStream extends ByteArrayOutputStream { + Cipher cipher; + byte oneByte[] = { 0 }; + + public CipherByteArrayOutputStream() throws GeneralSecurityException { + setBlock(0); + } + + public byte[] getBuf() { + return buf; + } + + public void setSize(int count) { + this.count = count; + } + + public void setBlock(int block) throws GeneralSecurityException { + cipher = initCipherForBlock(cipher, block); + } + + public void write(int b) { + try { + oneByte[0] = (byte)b; + cipher.update(oneByte, 0, 1, oneByte, 0); + super.write(oneByte); + } catch (Exception e) { + throw new EncryptedDocumentException(e); + } + } + + public void write(byte[] b, int off, int len) { + try { + cipher.update(b, off, len, b, off); + super.write(b, off, len); + } catch (Exception e) { + throw new EncryptedDocumentException(e); + } + } + + } +} diff --git a/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java index 86e31fb7a2..2b2c75b520 100644 --- a/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java +++ b/src/java/org/apache/poi/poifs/crypt/standard/StandardDecryptor.java @@ -34,7 +34,7 @@ import org.apache.poi.poifs.crypt.ChainingMode; import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.Decryptor; import org.apache.poi.poifs.crypt.EncryptionHeader; -import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.EncryptionInfoBuilder; import org.apache.poi.poifs.crypt.EncryptionVerifier; import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.filesystem.DirectoryNode; @@ -47,12 +47,12 @@ import org.apache.poi.util.LittleEndian; public class StandardDecryptor extends Decryptor { private long _length = -1; - protected StandardDecryptor(EncryptionInfo info) { - super(info); + protected StandardDecryptor(EncryptionInfoBuilder builder) { + super(builder); } public boolean verifyPassword(String password) { - EncryptionVerifier ver = info.getVerifier(); + EncryptionVerifier ver = builder.getVerifier(); SecretKey skey = generateSecretKey(password, ver, getKeySizeInBytes()); Cipher cipher = getCipher(skey); @@ -64,7 +64,11 @@ public class StandardDecryptor extends Decryptor { byte[] calcVerifierHash = sha1.digest(verifier); byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash(); byte decryptedVerifierHash[] = cipher.doFinal(encryptedVerifierHash); - byte[] verifierHash = truncateOrPad(decryptedVerifierHash, calcVerifierHash.length); + + // see 2.3.4.9 Password Verification (Standard Encryption) + // ... The number of bytes used by the encrypted Verifier hash MUST be 32 ... + // TODO: check and trim/pad the hashes to 32 + byte[] verifierHash = Arrays.copyOf(decryptedVerifierHash, calcVerifierHash.length); if (Arrays.equals(calcVerifierHash, verifierHash)) { setSecretKey(skey); @@ -93,7 +97,7 @@ public class StandardDecryptor extends Decryptor { System.arraycopy(x1, 0, x3, 0, x1.length); System.arraycopy(x2, 0, x3, x1.length, x2.length); - byte[] key = truncateOrPad(x3, keySize); + byte[] key = Arrays.copyOf(x3, keySize); SecretKey skey = new SecretKeySpec(key, ver.getCipherAlgorithm().jceId); return skey; @@ -111,24 +115,8 @@ public class StandardDecryptor extends Decryptor { return sha1.digest(buff); } - /** - * Returns a byte array of the requested length, - * truncated or zero padded as needed. - * Behaves like Arrays.copyOf in Java 1.6 - */ - protected static byte[] truncateOrPad(byte[] source, int length) { - byte[] result = new byte[length]; - System.arraycopy(source, 0, result, 0, Math.min(length, source.length)); - if(length > source.length) { - for(int i=source.length; i * @@ -33,6 +35,7 @@ public class LittleEndianInputStream extends FilterInputStream implements Little public LittleEndianInputStream(InputStream is) { super(is); } + public int available() { try { return super.available(); @@ -40,86 +43,75 @@ public class LittleEndianInputStream extends FilterInputStream implements Little throw new RuntimeException(e); } } + public byte readByte() { return (byte)readUByte(); } + public int readUByte() { - int ch; + byte buf[] = new byte[1]; try { - ch = in.read(); + checkEOF(read(buf), 1); } catch (IOException e) { throw new RuntimeException(e); } - checkEOF(ch); - return ch; + return LittleEndian.getUByte(buf); } + public double readDouble() { return Double.longBitsToDouble(readLong()); } + public int readInt() { - int ch1; - int ch2; - int ch3; - int ch4; + byte buf[] = new byte[LittleEndianConsts.INT_SIZE]; try { - ch1 = in.read(); - ch2 = in.read(); - ch3 = in.read(); - ch4 = in.read(); + checkEOF(read(buf), buf.length); } catch (IOException e) { throw new RuntimeException(e); } - checkEOF(ch1 | ch2 | ch3 | ch4); - return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0); + return LittleEndian.getInt(buf); } + + /** + * get an unsigned int value from an InputStream + * + * @return the unsigned int (32-bit) value + * @exception IOException + * will be propagated back to the caller + * @exception BufferUnderrunException + * if the stream cannot provide enough bytes + */ + public long readUInt() { + long retNum = readInt(); + return retNum & 0x00FFFFFFFFl; + } + public long readLong() { - int b0; - int b1; - int b2; - int b3; - int b4; - int b5; - int b6; - int b7; + byte buf[] = new byte[LittleEndianConsts.LONG_SIZE]; try { - b0 = in.read(); - b1 = in.read(); - b2 = in.read(); - b3 = in.read(); - b4 = in.read(); - b5 = in.read(); - b6 = in.read(); - b7 = in.read(); + checkEOF(read(buf), LittleEndianConsts.LONG_SIZE); } catch (IOException e) { throw new RuntimeException(e); } - checkEOF(b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7); - return (((long)b7 << 56) + - ((long)b6 << 48) + - ((long)b5 << 40) + - ((long)b4 << 32) + - ((long)b3 << 24) + - (b2 << 16) + - (b1 << 8) + - (b0 << 0)); + return LittleEndian.getLong(buf); } + public short readShort() { return (short)readUShort(); } + public int readUShort() { - int ch1; - int ch2; + byte buf[] = new byte[LittleEndianConsts.SHORT_SIZE]; try { - ch1 = in.read(); - ch2 = in.read(); + checkEOF(read(buf), LittleEndianConsts.SHORT_SIZE); } catch (IOException e) { throw new RuntimeException(e); } - checkEOF(ch1 | ch2); - return (ch2 << 8) + (ch1 << 0); + return LittleEndian.getUShort(buf); } - private static void checkEOF(int value) { - if (value <0) { + + private static void checkEOF(int actualBytes, int expectedBytes) { + if (expectedBytes != 0 && (actualBytes == -1 || actualBytes != expectedBytes)) { throw new RuntimeException("Unexpected end-of-file"); } } @@ -129,16 +121,10 @@ public class LittleEndianInputStream extends FilterInputStream implements Little } public void readFully(byte[] buf, int off, int len) { - int max = off+len; - for(int i=off; i 0) { - if (_chunk == null) { - try { - _chunk = nextChunk(); - } catch (GeneralSecurityException e) { - throw new EncryptedDocumentException(e.getMessage()); - } - } - int count = (int)(4096L - (_pos & 0xfff)); - int avail = available(); - if (avail == 0) { - return total; - } - count = Math.min(avail, Math.min(count, len)); - System.arraycopy(_chunk, (int)(_pos & 0xfff), b, off, count); - off += count; - len -= count; - _pos += count; - if ((_pos & 0xfff) == 0) - _chunk = null; - total += count; - } - - return total; - } - - public long skip(long n) throws IOException { - long start = _pos; - long skip = Math.min(available(), n); - - if ((((_pos + skip) ^ start) & ~0xfff) != 0) - _chunk = null; - _pos += skip; - return skip; - } - - public int available() throws IOException { return (int)(_size - _pos); } - public void close() throws IOException { _stream.close(); } - public boolean markSupported() { return false; } - - private byte[] nextChunk() throws GeneralSecurityException, IOException { - int index = (int)(_pos >> 12); - byte[] blockKey = new byte[4]; - LittleEndian.putInt(blockKey, 0, index); - EncryptionHeader header = info.getHeader(); - byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, getBlockSizeInBytes()); - AlgorithmParameterSpec aps; - if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) { - aps = new RC2ParameterSpec(getSecretKey().getEncoded().length*8, iv); - } else { - aps = new IvParameterSpec(iv); - } - - _cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), aps); - if (_lastIndex != index) - _stream.skip((index - _lastIndex) << 12); - - byte[] block = new byte[Math.min(_stream.available(), 4096)]; - _stream.read(block); - _lastIndex = index + 1; - return _cipher.doFinal(block); + // 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; - } } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java index 10ca07674f..b778c1032a 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionInfoBuilder.java @@ -26,7 +26,7 @@ import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.crypt.EncryptionInfoBuilder; import org.apache.poi.poifs.crypt.EncryptionMode; import org.apache.poi.poifs.crypt.HashAlgorithm; -import org.apache.poi.poifs.filesystem.DocumentInputStream; +import org.apache.poi.util.LittleEndianInput; import org.apache.xmlbeans.XmlException; import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument; @@ -39,10 +39,10 @@ public class AgileEncryptionInfoBuilder implements EncryptionInfoBuilder { AgileDecryptor decryptor; AgileEncryptor encryptor; - public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException { + public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException { this.info = info; - EncryptionDocument ed = parseDescriptor(dis); + EncryptionDocument ed = parseDescriptor((InputStream)dis); header = new AgileEncryptionHeader(ed); verifier = new AgileEncryptionVerifier(ed); if (info.getVersionMajor() == EncryptionMode.agile.versionMajor diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java index ee3b71036f..51ced4c2cc 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/agile/AgileEncryptor.java @@ -16,11 +16,11 @@ ==================================================================== */ package org.apache.poi.poifs.crypt.agile; -import static org.apache.poi.poifs.crypt.CryptoFunctions.generateIv; import static org.apache.poi.poifs.crypt.CryptoFunctions.getBlock0; import static org.apache.poi.poifs.crypt.CryptoFunctions.getCipher; import static org.apache.poi.poifs.crypt.CryptoFunctions.getMessageDigest; import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword; +import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry; import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.getNextBlockSize; import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.hashInput; import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kCryptoKeyBlock; @@ -32,16 +32,12 @@ import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kVerifierInputBloc import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FilterOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.SecureRandom; import java.security.cert.CertificateEncodingException; -import java.security.spec.AlgorithmParameterSpec; import java.util.HashMap; import java.util.Map; import java.util.Random; @@ -49,28 +45,20 @@ import java.util.Random; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.RC2ParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.apache.poi.EncryptedDocumentException; -import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.ChunkedCipherOutputStream; import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.DataSpaceMapUtils; -import org.apache.poi.poifs.crypt.EncryptionHeader; import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.crypt.Encryptor; import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry; +import org.apache.poi.poifs.crypt.standard.EncryptionRecord; import org.apache.poi.poifs.filesystem.DirectoryNode; -import org.apache.poi.poifs.filesystem.POIFSWriterEvent; -import org.apache.poi.poifs.filesystem.POIFSWriterListener; -import org.apache.poi.util.IOUtils; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianByteArrayOutputStream; -import org.apache.poi.util.LittleEndianConsts; -import org.apache.poi.util.LittleEndianOutputStream; -import org.apache.poi.util.TempFile; import org.apache.xmlbeans.XmlOptions; import com.microsoft.schemas.office.x2006.encryption.CTDataIntegrity; @@ -87,9 +75,7 @@ import com.microsoft.schemas.office.x2006.keyEncryptor.password.CTPasswordKeyEnc public class AgileEncryptor extends Encryptor { private final AgileEncryptionInfoBuilder builder; - @SuppressWarnings("unused") private byte integritySalt[]; - private Mac integrityMD; private byte pwHash[]; protected AgileEncryptor(AgileEncryptionInfoBuilder builder) { @@ -214,10 +200,6 @@ public class AgileEncryptor extends Encryptor { byte encryptedHmacKey[] = cipher.doFinal(filledSalt); header.setEncryptedHmacKey(encryptedHmacKey); - this.integrityMD = CryptoFunctions.getMac(hashAlgo); - this.integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId)); - - cipher = Cipher.getInstance("RSA"); for (AgileCertificateEntry ace : ver.getCertificates()) { cipher.init(Cipher.ENCRYPT_MODE, ace.x509.getPublicKey()); @@ -234,182 +216,59 @@ public class AgileEncryptor extends Encryptor { public OutputStream getDataStream(DirectoryNode dir) throws IOException, GeneralSecurityException { // TODO: initialize headers - OutputStream countStream = new ChunkedCipherOutputStream(dir); + AgileCipherOutputStream countStream = new AgileCipherOutputStream(dir); return countStream; } /** - * 2.3.4.15 Data Encryption (Agile Encryption) + * Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), + * which the DataIntegrity element will verify by using the Salt generated in step 2 as the key. + * Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be + * used as the message. * - * The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly - * random access while allowing CBC modes to be used in the encryption process. - * The initialization vector for the encryption process MUST be obtained by using the zero-based - * segment number as a blockKey and the binary form of the KeyData.saltValue as specified in - * section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer. - * Data blocks MUST then be encrypted by using the initialization vector and the intermediate key - * obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the - * KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to - * the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note - * that the StreamSize field of the EncryptedPackage field specifies the number of bytes of - * unencrypted data as specified in section 2.3.4.4. - */ - private class ChunkedCipherOutputStream extends FilterOutputStream implements POIFSWriterListener { - private long _pos = 0; - private final byte[] _chunk = new byte[4096]; - private Cipher _cipher; - private final File fileOut; - protected final DirectoryNode dir; - - public ChunkedCipherOutputStream(DirectoryNode dir) throws IOException { - super(null); - fileOut = TempFile.createTempFile("encrypted_package", "crypt"); - this.out = new FileOutputStream(fileOut); - this.dir = dir; - EncryptionHeader header = builder.getHeader(); - _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE); - } - - public void write(int b) throws IOException { - write(new byte[]{(byte)b}); - } - - public void write(byte[] b) throws IOException { - write(b, 0, b.length); - } - - public void write(byte[] b, int off, int len) - throws IOException { - if (len == 0) return; - - if (len < 0 || b.length < off+len) { - throw new IOException("not enough bytes in your input buffer"); - } - - while (len > 0) { - int posInChunk = (int)(_pos & 0xfff); - int nextLen = Math.min(4096-posInChunk, len); - System.arraycopy(b, off, _chunk, posInChunk, nextLen); - _pos += nextLen; - off += nextLen; - len -= nextLen; - if ((_pos & 0xfff) == 0) { - writeChunk(); - } - } - } - - private void writeChunk() throws IOException { - EncryptionHeader header = builder.getHeader(); - int blockSize = header.getBlockSize(); - - int posInChunk = (int)(_pos & 0xfff); - // normally posInChunk is 0, i.e. on the next chunk (-> index-1) - // but if called on close(), posInChunk is somewhere within the chunk data - int index = (int)(_pos >> 12); - if (posInChunk==0) { - index--; - posInChunk = 4096; - } else { - // pad the last chunk - _cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE, "PKCS5Padding"); - } + * Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes: + * 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33. + **/ + protected void updateIntegrityHMAC(File tmpFile, int oleStreamSize) throws GeneralSecurityException, IOException { + // as the integrity hmac needs to contain the StreamSize, + // it's not possible to calculate it on-the-fly while buffering + // TODO: add stream size parameter to getDataStream() + AgileEncryptionVerifier ver = builder.getVerifier(); + HashAlgorithm hashAlgo = ver.getHashAlgorithm(); + Mac integrityMD = CryptoFunctions.getMac(hashAlgo); + integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId)); - byte[] blockKey = new byte[4]; - LittleEndian.putInt(blockKey, 0, index); - byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, blockSize); - try { - AlgorithmParameterSpec aps; - if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) { - aps = new RC2ParameterSpec(getSecretKey().getEncoded().length*8, iv); - } else { - aps = new IvParameterSpec(iv); - } - - _cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), aps); - int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk); - out.write(_chunk, 0, ciLen); - } catch (GeneralSecurityException e) { - throw (IOException)new IOException().initCause(e); - } - } + byte buf[] = new byte[1024]; + LittleEndian.putLong(buf, 0, oleStreamSize); + integrityMD.update(buf, 0, LittleEndian.LONG_SIZE); - public void close() throws IOException { - writeChunk(); - super.close(); - writeToPOIFS(); - } - - void writeToPOIFS() throws IOException { - DataSpaceMapUtils.addDefaultDataSpace(dir); - - /** - * Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message), - * which the DataIntegrity element will verify by using the Salt generated in step 2 as the key. - * Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be - * used as the message. - * - * Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes: - * 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33. - **/ - byte buf[] = new byte[4096]; - LittleEndian.putLong(buf, 0, _pos); - integrityMD.update(buf, 0, LittleEndianConsts.LONG_SIZE); - - InputStream fis = new FileInputStream(fileOut); - for (int readBytes; (readBytes = fis.read(buf)) != -1; integrityMD.update(buf, 0, readBytes)); - fis.close(); - - AgileEncryptionHeader header = builder.getHeader(); - int blockSize = header.getBlockSize(); - - byte hmacValue[] = integrityMD.doFinal(); - byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, header.getBlockSize()); - Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE); - try { - byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize)); - byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled); - header.setEncryptedHmacValue(encryptedHmacValue); - } catch (GeneralSecurityException e) { - throw new EncryptedDocumentException(e); - } - - createEncryptionInfoEntry(dir); - - int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE); - dir.createDocument("EncryptedPackage", oleStreamSize, this); - // TODO: any properties??? - } - - public void processPOIFSWriterEvent(POIFSWriterEvent event) { - try { - LittleEndianOutputStream leos = new LittleEndianOutputStream(event.getStream()); - - // StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data - // encrypted within the EncryptedData field, not including the size of the StreamSize field. - // Note that the actual size of the \EncryptedPackage stream (1) can be larger than this - // value, depending on the block size of the chosen encryption algorithm - leos.writeLong(_pos); - - FileInputStream fis = new FileInputStream(fileOut); - IOUtils.copy(fis, leos); - fis.close(); - fileOut.delete(); - - leos.close(); - } catch (IOException e) { - throw new EncryptedDocumentException(e); - } + FileInputStream fis = new FileInputStream(tmpFile); + int readBytes; + while ((readBytes = fis.read(buf)) != -1) { + integrityMD.update(buf, 0, readBytes); } - } - - protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException { - final CTKeyEncryptor.Uri.Enum passwordUri = - CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD; - final CTKeyEncryptor.Uri.Enum certificateUri = - CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE; + fis.close(); + + byte hmacValue[] = integrityMD.doFinal(); - AgileEncryptionVerifier ver = builder.getVerifier(); AgileEncryptionHeader header = builder.getHeader(); + int blockSize = header.getBlockSize(); + byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, blockSize); + Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE); + byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize)); + byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled); + + header.setEncryptedHmacValue(encryptedHmacValue); + } + + private final CTKeyEncryptor.Uri.Enum passwordUri = + CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD; + private final CTKeyEncryptor.Uri.Enum certificateUri = + CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE; + + protected EncryptionDocument createEncryptionDocument() { + AgileEncryptionVerifier ver = builder.getVerifier(); + AgileEncryptionHeader header = builder.getHeader(); EncryptionDocument ed = EncryptionDocument.Factory.newInstance(); CTEncryption edRoot = ed.addNewEncryption(); @@ -485,6 +344,10 @@ public class AgileEncryptor extends Encryptor { certData.setCertVerifier(ace.certVerifier); } + return ed; + } + + protected void marshallEncryptionDocument(EncryptionDocument ed, LittleEndianByteArrayOutputStream os) { XmlOptions xo = new XmlOptions(); xo.setCharacterEncoding("UTF-8"); Map nsMap = new HashMap(); @@ -494,33 +357,82 @@ public class AgileEncryptor extends Encryptor { xo.setSaveSuggestedPrefixes(nsMap); xo.setSaveNamespacesFirst(); xo.setSaveAggressiveNamespaces(); - // setting standalone doesn't work with xmlbeans-2.3 + + // setting standalone doesn't work with xmlbeans-2.3 & 2.6 + // ed.documentProperties().setStandalone(true); xo.setSaveNoXmlDecl(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - bos.write("\r\n".getBytes("UTF-8")); - ed.save(bos, xo); + try { + bos.write("\r\n".getBytes("UTF-8")); + ed.save(bos, xo); + os.write(bos.toByteArray()); + } catch (IOException e) { + throw new EncryptedDocumentException("error marshalling encryption info document", e); + } + } - final byte buf[] = new byte[5000]; - LittleEndianByteArrayOutputStream leos = new LittleEndianByteArrayOutputStream(buf, 0); - EncryptionInfo info = builder.getInfo(); + protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile) + throws IOException, GeneralSecurityException { + DataSpaceMapUtils.addDefaultDataSpace(dir); - // EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where - // Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004 - leos.writeShort(info.getVersionMajor()); - leos.writeShort(info.getVersionMinor()); - // Reserved (4 bytes): A value that MUST be 0x00000040 - leos.writeInt(info.getEncryptionFlags()); - leos.write(bos.toByteArray()); - - dir.createDocument("EncryptionInfo", leos.getWriteIndex(), new POIFSWriterListener() { - public void processPOIFSWriterEvent(POIFSWriterEvent event) { - try { - event.getStream().write(buf, 0, event.getLimit()); - } catch (IOException e) { - throw new EncryptedDocumentException(e); - } + final EncryptionInfo info = builder.getInfo(); + + EncryptionRecord er = new EncryptionRecord(){ + public void write(LittleEndianByteArrayOutputStream bos) { + // EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where + // Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004 + bos.writeShort(info.getVersionMajor()); + bos.writeShort(info.getVersionMinor()); + // Reserved (4 bytes): A value that MUST be 0x00000040 + bos.writeInt(info.getEncryptionFlags()); + + EncryptionDocument ed = createEncryptionDocument(); + marshallEncryptionDocument(ed, bos); } - }); + }; + + createEncryptionEntry(dir, "EncryptionInfo", er); } + + + /** + * 2.3.4.15 Data Encryption (Agile Encryption) + * + * The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly + * random access while allowing CBC modes to be used in the encryption process. + * The initialization vector for the encryption process MUST be obtained by using the zero-based + * segment number as a blockKey and the binary form of the KeyData.saltValue as specified in + * section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer. + * Data blocks MUST then be encrypted by using the initialization vector and the intermediate key + * obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the + * KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to + * the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note + * that the StreamSize field of the EncryptedPackage field specifies the number of bytes of + * unencrypted data as specified in section 2.3.4.4. + */ + private class AgileCipherOutputStream extends ChunkedCipherOutputStream { + public AgileCipherOutputStream(DirectoryNode dir) throws IOException, GeneralSecurityException { + super(dir, 4096); + } + + @Override + protected Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk) + throws GeneralSecurityException { + return AgileDecryptor.initCipherForBlock(existing, block, lastChunk, builder, getSecretKey(), Cipher.ENCRYPT_MODE); + } + + @Override + protected void calculateChecksum(File fileOut, int oleStreamSize) + throws GeneralSecurityException, IOException { + // integrityHMAC needs to be updated before the encryption document is created + updateIntegrityHMAC(fileOut, oleStreamSize); + } + + @Override + protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile) + throws IOException, GeneralSecurityException { + AgileEncryptor.this.createEncryptionInfoEntry(dir, tmpFile); + } + } + } diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java index 131a5c2e49..0d441d68da 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestAgileEncryptionParameters.java @@ -53,7 +53,7 @@ public class TestAgileEncryptionParameters { @Parameter(value = 2) public ChainingMode cm; - @Parameters + @Parameters(name="{0} {1} {2}") public static Collection data() { CipherAlgorithm caList[] = { CipherAlgorithm.aes128, CipherAlgorithm.aes192, CipherAlgorithm.aes256, CipherAlgorithm.rc2, CipherAlgorithm.des, CipherAlgorithm.des3 }; HashAlgorithm haList[] = { HashAlgorithm.sha1, HashAlgorithm.sha256, HashAlgorithm.sha384, HashAlgorithm.sha512, HashAlgorithm.md5 }; @@ -86,7 +86,7 @@ public class TestAgileEncryptionParameters { ByteArrayOutputStream bos = new ByteArrayOutputStream(); POIFSFileSystem fsEnc = new POIFSFileSystem(); - EncryptionInfo infoEnc = new EncryptionInfo(fsEnc, EncryptionMode.agile, ca, ha, -1, -1, cm); + EncryptionInfo infoEnc = new EncryptionInfo(EncryptionMode.agile, ca, ha, -1, -1, cm); Encryptor enc = infoEnc.getEncryptor(); enc.confirmPassword("foobaa"); OutputStream os = enc.getDataStream(fsEnc); diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java index fa04dde5e9..ed7df2ed81 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestDecryptor.java @@ -29,6 +29,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.poi.POIDataSamples; +import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.util.IOUtils; @@ -60,7 +61,7 @@ public class TestDecryptor { d.verifyPassword(Decryptor.DEFAULT_PASSWORD); - zipOk(fs, d); + zipOk(fs.getRoot(), d); } @Test @@ -75,21 +76,22 @@ public class TestDecryptor { assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD)); - zipOk(fs, d); + zipOk(fs.getRoot(), d); } - private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException { - ZipInputStream zin = new ZipInputStream(d.getDataStream(fs)); + private void zipOk(DirectoryNode root, Decryptor d) throws IOException, GeneralSecurityException { + ZipInputStream zin = new ZipInputStream(d.getDataStream(root)); while (true) { ZipEntry entry = zin.getNextEntry(); - if (entry==null) { - break; - } - - while (zin.available()>0) { - zin.skip(zin.available()); - } + if (entry==null) break; + // crc32 is checked within zip-stream + if (entry.isDirectory()) continue; + zin.skip(entry.getSize()); + byte buf[] = new byte[10]; + int readBytes = zin.read(buf); + // zin.available() doesn't work for entries + assertEquals("size failed for "+entry.getName(), -1, readBytes); } zin.close(); diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java index 3584b8f9bd..37deba5e82 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestEncryptor.java @@ -16,9 +16,8 @@ ==================================================================== */ package org.apache.poi.poifs.crypt; -import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; @@ -49,7 +48,44 @@ import org.junit.Test; public class TestEncryptor { @Test - public void testAgileEncryption() throws Exception { + public void binaryRC4Encryption() throws Exception { + // please contribute a real sample file, which is binary rc4 encrypted + // ... at least the output can be opened in Excel Viewer + String password = "pass"; + + InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleMultiCell.xlsx"); + ByteArrayOutputStream payloadExpected = new ByteArrayOutputStream(); + IOUtils.copy(is, payloadExpected); + is.close(); + + POIFSFileSystem fs = new POIFSFileSystem(); + EncryptionInfo ei = new EncryptionInfo(EncryptionMode.binaryRC4); + Encryptor enc = ei.getEncryptor(); + enc.confirmPassword(password); + + OutputStream os = enc.getDataStream(fs.getRoot()); + payloadExpected.writeTo(os); + os.close(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + fs.writeFilesystem(bos); + + fs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray())); + ei = new EncryptionInfo(fs); + Decryptor dec = ei.getDecryptor(); + boolean b = dec.verifyPassword(password); + assertTrue(b); + + ByteArrayOutputStream payloadActual = new ByteArrayOutputStream(); + is = dec.getDataStream(fs.getRoot()); + IOUtils.copy(is,payloadActual); + is.close(); + + assertArrayEquals(payloadExpected.toByteArray(), payloadActual.toByteArray()); + } + + @Test + public void agileEncryption() throws Exception { int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES"); Assume.assumeTrue("Please install JCE Unlimited Strength Jurisdiction Policy files for AES 256", maxKeyLen == 2147483647); @@ -92,7 +128,7 @@ public class TestEncryptor { POIFSFileSystem fs = new POIFSFileSystem(); EncryptionInfo infoActual = new EncryptionInfo( - fs, EncryptionMode.agile + EncryptionMode.agile , infoExpected.getVerifier().getCipherAlgorithm() , infoExpected.getVerifier().getHashAlgorithm() , infoExpected.getHeader().getKeySize() @@ -134,14 +170,14 @@ public class TestEncryptor { AgileEncryptionHeader aehExpected = (AgileEncryptionHeader)infoExpected.getHeader(); AgileEncryptionHeader aehActual = (AgileEncryptionHeader)infoActual.getHeader(); - assertThat(aehExpected.getEncryptedHmacKey(), equalTo(aehActual.getEncryptedHmacKey())); + assertArrayEquals(aehExpected.getEncryptedHmacKey(), aehActual.getEncryptedHmacKey()); assertEquals(decPackLenExpected, decPackLenActual); - assertThat(payloadExpected, equalTo(payloadActual)); - assertThat(encPackExpected, equalTo(encPackActual)); + assertArrayEquals(payloadExpected, payloadActual); + assertArrayEquals(encPackExpected, encPackActual); } @Test - public void testStandardEncryption() throws Exception { + public void standardEncryption() throws Exception { File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx"); String pass = "solrcell"; @@ -170,7 +206,7 @@ public class TestEncryptor { POIFSFileSystem fs = new POIFSFileSystem(); EncryptionInfo infoActual = new EncryptionInfo( - fs, EncryptionMode.standard + EncryptionMode.standard , infoExpected.getVerifier().getCipherAlgorithm() , infoExpected.getVerifier().getHashAlgorithm() , infoExpected.getHeader().getKeySize() @@ -181,15 +217,15 @@ public class TestEncryptor { Encryptor e = Encryptor.getInstance(infoActual); e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, null); - assertThat(infoExpected.getVerifier().getEncryptedVerifier(), equalTo(infoActual.getVerifier().getEncryptedVerifier())); - assertThat(infoExpected.getVerifier().getEncryptedVerifierHash(), equalTo(infoActual.getVerifier().getEncryptedVerifierHash())); + assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifier(), infoActual.getVerifier().getEncryptedVerifier()); + assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifierHash(), infoActual.getVerifier().getEncryptedVerifierHash()); // now we use a newly generated salt/verifier and check // if the file content is still the same fs = new POIFSFileSystem(); infoActual = new EncryptionInfo( - fs, EncryptionMode.standard + EncryptionMode.standard , infoExpected.getVerifier().getCipherAlgorithm() , infoExpected.getVerifier().getHashAlgorithm() , infoExpected.getHeader().getKeySize() @@ -227,12 +263,12 @@ public class TestEncryptor { nfs.close(); byte payloadActual[] = bos.toByteArray(); - assertThat(payloadExpected, equalTo(payloadActual)); + assertArrayEquals(payloadExpected, payloadActual); } @Test @Ignore - public void testInPlaceRewrite() throws Exception { + public void inPlaceRewrite() throws Exception { File f = TempFile.createTempFile("protected_agile", ".docx"); // File f = new File("protected_agile.docx"); FileOutputStream fos = new FileOutputStream(f); @@ -264,10 +300,10 @@ public class TestEncryptor { private void listEntry(DocumentNode de, String ext, String path) throws IOException { - path += "\\" + de.getName().replace('\u0006', '_'); + path += "\\" + de.getName().replaceAll("[\\p{Cntrl}]", "_"); System.out.println(ext+": "+path+" ("+de.getSize()+" bytes)"); - String name = de.getName().replace('\u0006', '_'); + String name = de.getName().replaceAll("[\\p{Cntrl}]", "_"); InputStream is = ((DirectoryNode)de.getParent()).createDocumentInputStream(de); FileOutputStream fos = new FileOutputStream("solr."+name+"."+ext); diff --git a/src/scratchpad/src/org/apache/poi/hslf/EncryptedSlideShow.java b/src/scratchpad/src/org/apache/poi/hslf/EncryptedSlideShow.java index d8839241cc..07a01aa244 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/EncryptedSlideShow.java +++ b/src/scratchpad/src/org/apache/poi/hslf/EncryptedSlideShow.java @@ -17,117 +17,478 @@ 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 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 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 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 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 slideLocations = new TreeMap(); + TreeMap recordMap = new TreeMap(); + List obsoleteOffsets = new ArrayList(); + 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 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 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 recordList = new ArrayList(); + 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 tm = new TreeMap(ptr.getSlideLocationsLookup()); + ptr.clear(); + int maxSlideId = -1; + for (Map.Entry 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 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; - } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/HSLFSlideShow.java b/src/scratchpad/src/org/apache/poi/hslf/HSLFSlideShow.java index 8669caf2cd..3c63317c62 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/HSLFSlideShow.java +++ b/src/scratchpad/src/org/apache/poi/hslf/HSLFSlideShow.java @@ -23,6 +23,7 @@ import java.io.FileInputStream; 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; @@ -32,10 +33,11 @@ import java.util.NavigableMap; 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; @@ -45,6 +47,7 @@ import org.apache.poi.hslf.record.RecordTypes; 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; @@ -182,13 +185,6 @@ public final class HSLFSlideShow extends POIDocument { // 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(); @@ -278,6 +274,7 @@ public final class HSLFSlideShow extends POIDocument { NavigableMap records = new TreeMap(); // offset -> record Map persistIds = new HashMap(); // offset -> persistId initRecordOffsets(docstream, usrOffset, records, persistIds); + EncryptedSlideShow decryptData = new EncryptedSlideShow(docstream, records); for (Map.Entry entry : records.entrySet()) { Integer offset = entry.getKey(); @@ -286,6 +283,7 @@ public final class HSLFSlideShow extends POIDocument { 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); } @@ -335,6 +333,16 @@ public final class HSLFSlideShow extends POIDocument { } } + public DocumentEncryptionAtom getDocumentEncryptionAtom() { + for (Record r : _records) { + if (r instanceof DocumentEncryptionAtom) { + return (DocumentEncryptionAtom)r; + } + } + return null; + } + + /** * Find the "Current User" stream, and load it */ @@ -353,6 +361,7 @@ public final class HSLFSlideShow extends POIDocument { 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. @@ -363,6 +372,8 @@ public final class HSLFSlideShow extends POIDocument { // 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()]; @@ -375,6 +386,8 @@ public final class HSLFSlideShow extends POIDocument { // 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); @@ -422,7 +435,21 @@ public final class HSLFSlideShow extends POIDocument { 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. @@ -444,55 +471,67 @@ public final class HSLFSlideShow extends POIDocument { // 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 persistIds = new HashMap(); + for (Map.Entry 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)); } } @@ -504,7 +543,7 @@ public final class HSLFSlideShow extends POIDocument { } currentUser.setCurrentEditOffset(usr.getLastOnDiskOffset()); } - + /** * Writes out the slideshow file the is represented by an instance * of this class. @@ -529,6 +568,16 @@ public final class HSLFSlideShow extends POIDocument { * 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(); @@ -537,8 +586,8 @@ public final class HSLFSlideShow extends POIDocument { // 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 @@ -546,27 +595,28 @@ public final class HSLFSlideShow extends POIDocument { 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"); } @@ -580,8 +630,44 @@ public final class HSLFSlideShow extends POIDocument { 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 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 @@ -688,4 +774,30 @@ public final class HSLFSlideShow extends POIDocument { } 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; + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/dev/SlideIdListing.java b/src/scratchpad/src/org/apache/poi/hslf/dev/SlideIdListing.java index 48d8352eaf..010f424c33 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/dev/SlideIdListing.java +++ b/src/scratchpad/src/org/apache/poi/hslf/dev/SlideIdListing.java @@ -17,15 +17,23 @@ 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 @@ -122,10 +130,10 @@ public final class SlideIdListing { // Check the sheet offsets int[] sheetIDs = pph.getKnownSlideIDs(); - Hashtable sheetOffsets = pph.getSlideLocationsLookup(); + Map sheetOffsets = pph.getSlideLocationsLookup(); for(int j=0; j sheetOffsets = pph.getSlideLocationsLookup(); for(int j=0; j oldToNewReferencesLookup) { + + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/PersistPtrHolder.java b/src/scratchpad/src/org/apache/poi/hslf/record/PersistPtrHolder.java index de341acd1f..3095b5e422 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/PersistPtrHolder.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/PersistPtrHolder.java @@ -17,13 +17,17 @@ 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 @@ -49,12 +53,14 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom * that knows about a given slide to find the right location */ private Hashtable _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 _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. @@ -63,10 +69,9 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom */ public int[] getKnownSlideIDs() { int[] ids = new int[_slideLocations.size()]; - Enumeration e = _slideLocations.keys(); - for(int i=0; i 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 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"); } /** @@ -141,30 +116,27 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom // count * 32 bit offsets // Repeat as many times as you have data _slideLocations = new Hashtable(); - _slideOffsetDataLocation = new Hashtable(); _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 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 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 orderedSlideLocations = new TreeMap(_slideLocations); + + @SuppressWarnings("resource") + BufAccessBAOS bos = new BufAccessBAOS(); + byte intbuf[] = new byte[4]; + int lastPersistEntry = -1; + int lastSlideId = -1; + for (Map.Entry 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; + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/Record.java b/src/scratchpad/src/org/apache/poi/hslf/record/Record.java index eb6944ac69..78db7ee273 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/Record.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/Record.java @@ -74,7 +74,7 @@ public abstract class Record */ 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); } /** @@ -82,7 +82,7 @@ public abstract class Record */ 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); } diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/UserEditAtom.java b/src/scratchpad/src/org/apache/poi/hslf/record/UserEditAtom.java index 0a2c8a03ba..d0c8b563d7 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/UserEditAtom.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/UserEditAtom.java @@ -18,6 +18,8 @@ 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; @@ -42,7 +44,7 @@ public final class UserEditAtom extends PositionDependentRecordAtom private byte[] _header; private static long _type = 4085l; - private byte[] reserved; + private short unused; private int lastViewedSlideID; private int pptVersion; @@ -51,6 +53,7 @@ public final class UserEditAtom extends PositionDependentRecordAtom private int docPersistRef; private int maxPersistWritten; private short lastViewType; + private int encryptSessionPersistIdRef = -1; // Somewhat user facing getters public int getLastViewedSlideID() { return lastViewedSlideID; } @@ -61,12 +64,17 @@ public final class UserEditAtom extends PositionDependentRecordAtom 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 ********************** */ @@ -77,39 +85,56 @@ public final class UserEditAtom extends PositionDependentRecordAtom // 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-startorg.apache.poi.hslf.record. * * @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; - } } diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestCurrentUserAtom.java b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestCurrentUserAtom.java index ed1f19a030..05478e3374 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestCurrentUserAtom.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestCurrentUserAtom.java @@ -17,36 +17,33 @@ 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) ); @@ -66,20 +63,20 @@ public final class TestCurrentUserAtom extends TestCase { 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) diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java new file mode 100644 index 0000000000..067a94c9a7 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestDocumentEncryption.java @@ -0,0 +1,182 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hslf.record; + + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.security.MessageDigest; + +import org.apache.commons.codec.binary.Base64; +import org.apache.poi.POIDataSamples; +import org.apache.poi.hpsf.DocumentSummaryInformation; +import org.apache.poi.hpsf.PropertySet; +import org.apache.poi.hpsf.PropertySetFactory; +import org.apache.poi.hpsf.SummaryInformation; +import org.apache.poi.hslf.HSLFSlideShow; +import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException; +import org.apache.poi.hslf.model.Slide; +import org.apache.poi.hslf.usermodel.PictureData; +import org.apache.poi.hslf.usermodel.SlideShow; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.EncryptionInfo; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader; +import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests that DocumentEncryption works properly. + */ +public class TestDocumentEncryption { + POIDataSamples slTests = POIDataSamples.getSlideShowInstance(); + + @Before + public void resetPassword() { + Biff8EncryptionKey.setCurrentUserPassword(null); + } + + @Test + public void cryptoAPIDecryptionOther() throws Exception { + Biff8EncryptionKey.setCurrentUserPassword("hello"); + String encPpts[] = { + "Password_Protected-56-hello.ppt", + "Password_Protected-hello.ppt", + "Password_Protected-np-hello.ppt", + }; + + for (String pptFile : encPpts) { + try { + NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile(pptFile), true); + HSLFSlideShow hss = new HSLFSlideShow(fs); + new SlideShow(hss); + fs.close(); + } catch (EncryptedPowerPointFileException e) { + fail(pptFile+" can't be decrypted"); + } + } + } + + @Test + public void cryptoAPIChangeKeySize() throws Exception { + String pptFile = "cryptoapi-proc2356.ppt"; + Biff8EncryptionKey.setCurrentUserPassword("crypto"); + NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile(pptFile), true); + HSLFSlideShow hss = new HSLFSlideShow(fs); + // need to cache data (i.e. read all data) before changing the key size + PictureData picsExpected[] = hss.getPictures(); + hss.getDocumentSummaryInformation(); + EncryptionInfo ei = hss.getDocumentEncryptionAtom().getEncryptionInfo(); + ((CryptoAPIEncryptionHeader)ei.getHeader()).setKeySize(0x78); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + hss.write(bos); + fs.close(); + + fs = new NPOIFSFileSystem(new ByteArrayInputStream(bos.toByteArray())); + hss = new HSLFSlideShow(fs); + PictureData picsActual[] = hss.getPictures(); + fs.close(); + + assertEquals(picsExpected.length, picsActual.length); + for (int i=0; i