mirror of https://github.com/apache/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
This commit is contained in:
parent
2668385b17
commit
0839a097e3
|
@ -20,6 +20,7 @@ package org.apache.poi;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.List;
|
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.PropertySet;
|
||||||
import org.apache.poi.hpsf.PropertySetFactory;
|
import org.apache.poi.hpsf.PropertySetFactory;
|
||||||
import org.apache.poi.hpsf.SummaryInformation;
|
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.DirectoryEntry;
|
||||||
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||||
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
||||||
|
@ -163,14 +165,40 @@ public abstract class POIDocument {
|
||||||
* @return The value of the given property or null if it wasn't found.
|
* @return The value of the given property or null if it wasn't found.
|
||||||
*/
|
*/
|
||||||
protected PropertySet getPropertySet(String setName) {
|
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
|
//directory can be null when creating new documents
|
||||||
if (directory == null || !directory.hasEntry(setName))
|
if (dirNode == null || !dirNode.hasEntry(setName))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
DocumentInputStream dis;
|
DocumentInputStream dis;
|
||||||
try {
|
try {
|
||||||
// Find the entry, and get an input stream for it
|
// 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) {
|
} catch(IOException ie) {
|
||||||
// Oh well, doesn't exist
|
// Oh well, doesn't exist
|
||||||
logger.log(POILogger.WARN, "Error getting property set with name " + setName + "\n" + ie);
|
logger.log(POILogger.WARN, "Error getting property set with name " + setName + "\n" + ie);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,8 +20,8 @@ package org.apache.poi.poifs.crypt;
|
||||||
import org.apache.poi.EncryptedDocumentException;
|
import org.apache.poi.EncryptedDocumentException;
|
||||||
|
|
||||||
public enum CipherProvider {
|
public enum CipherProvider {
|
||||||
rc4("RC4", 1),
|
rc4("RC4", 1, "Microsoft Base Cryptographic Provider v1.0"),
|
||||||
aes("AES", 0x18);
|
aes("AES", 0x18, "Microsoft Enhanced RSA and AES Cryptographic Provider");
|
||||||
|
|
||||||
public static CipherProvider fromEcmaId(int ecmaId) {
|
public static CipherProvider fromEcmaId(int ecmaId) {
|
||||||
for (CipherProvider cp : CipherProvider.values()) {
|
for (CipherProvider cp : CipherProvider.values()) {
|
||||||
|
@ -32,8 +32,10 @@ public enum CipherProvider {
|
||||||
|
|
||||||
public final String jceId;
|
public final String jceId;
|
||||||
public final int ecmaId;
|
public final int ecmaId;
|
||||||
CipherProvider(String jceId, int ecmaId) {
|
public final String cipherProviderName;
|
||||||
|
CipherProvider(String jceId, int ecmaId, String cipherProviderName) {
|
||||||
this.jceId = jceId;
|
this.jceId = jceId;
|
||||||
this.ecmaId = ecmaId;
|
this.ecmaId = ecmaId;
|
||||||
|
this.cipherProviderName = cipherProviderName;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -30,12 +30,12 @@ import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
||||||
public abstract class Decryptor {
|
public abstract class Decryptor {
|
||||||
public static final String DEFAULT_PASSWORD="VelvetSweatshop";
|
public static final String DEFAULT_PASSWORD="VelvetSweatshop";
|
||||||
|
|
||||||
protected final EncryptionInfo info;
|
protected final EncryptionInfoBuilder builder;
|
||||||
private SecretKey secretKey;
|
private SecretKey secretKey;
|
||||||
private byte[] verifier, integrityHmacKey, integrityHmacValue;
|
private byte[] verifier, integrityHmacKey, integrityHmacValue;
|
||||||
|
|
||||||
protected Decryptor(EncryptionInfo info) {
|
protected Decryptor(EncryptionInfoBuilder builder) {
|
||||||
this.info = info;
|
this.builder = builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,7 +56,7 @@ public abstract class Decryptor {
|
||||||
throws GeneralSecurityException;
|
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)}.
|
* {@link #getDataStream(org.apache.poi.poifs.filesystem.DirectoryNode)}.
|
||||||
* Just reading to the end of the input stream is not sufficient because there are
|
* Just reading to the end of the input stream is not sufficient because there are
|
||||||
* normally padding bytes that must be discarded
|
* normally padding bytes that must be discarded
|
||||||
|
@ -120,4 +120,12 @@ public abstract class Decryptor {
|
||||||
protected void setIntegrityHmacValue(byte[] integrityHmacValue) {
|
protected void setIntegrityHmacValue(byte[] integrityHmacValue) {
|
||||||
this.integrityHmacValue = integrityHmacValue;
|
this.integrityHmacValue = integrityHmacValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected int getBlockSizeInBytes() {
|
||||||
|
return builder.getHeader().getBlockSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getKeySizeInBytes() {
|
||||||
|
return builder.getHeader().getKeySize()/8;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -17,15 +17,19 @@
|
||||||
package org.apache.poi.poifs.crypt;
|
package org.apache.poi.poifs.crypt;
|
||||||
|
|
||||||
import static org.apache.poi.poifs.crypt.EncryptionMode.agile;
|
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 static org.apache.poi.poifs.crypt.EncryptionMode.standard;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.apache.poi.EncryptedDocumentException;
|
import org.apache.poi.EncryptedDocumentException;
|
||||||
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||||
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
|
||||||
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
|
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
|
||||||
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
||||||
|
import org.apache.poi.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 Decryptor decryptor;
|
||||||
private final Encryptor encryptor;
|
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 {
|
public EncryptionInfo(POIFSFileSystem fs) throws IOException {
|
||||||
this(fs.getRoot());
|
this(fs.getRoot());
|
||||||
}
|
}
|
||||||
|
@ -48,18 +77,43 @@ public class EncryptionInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionInfo(DirectoryNode dir) throws IOException {
|
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();
|
versionMajor = dis.readShort();
|
||||||
versionMinor = dis.readShort();
|
versionMinor = dis.readShort();
|
||||||
encryptionFlags = dis.readInt();
|
|
||||||
|
if (!isCryptoAPI
|
||||||
EncryptionMode encryptionMode;
|
&& versionMajor == binaryRC4.versionMajor
|
||||||
if (versionMajor == agile.versionMajor
|
&& versionMinor == binaryRC4.versionMinor) {
|
||||||
&& versionMinor == agile.versionMinor
|
encryptionMode = binaryRC4;
|
||||||
&& encryptionFlags == agile.encryptionFlags) {
|
encryptionFlags = -1;
|
||||||
|
} else if (!isCryptoAPI
|
||||||
|
&& versionMajor == agile.versionMajor
|
||||||
|
&& versionMinor == agile.versionMinor){
|
||||||
encryptionMode = agile;
|
encryptionMode = agile;
|
||||||
} else {
|
encryptionFlags = dis.readInt();
|
||||||
|
} else if (!isCryptoAPI
|
||||||
|
&& 2 <= versionMajor && versionMajor <= 4
|
||||||
|
&& versionMinor == standard.versionMinor) {
|
||||||
encryptionMode = standard;
|
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;
|
EncryptionInfoBuilder eib;
|
||||||
|
@ -75,22 +129,35 @@ public class EncryptionInfo {
|
||||||
decryptor = eib.getDecryptor();
|
decryptor = eib.getDecryptor();
|
||||||
encryptor = eib.getEncryptor();
|
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
|
* @deprecated use constructor without dir parameter
|
||||||
, EncryptionMode encryptionMode
|
*/
|
||||||
) throws EncryptedDocumentException {
|
@Deprecated
|
||||||
this(dir, encryptionMode, null, null, -1, -1, null);
|
public EncryptionInfo(DirectoryNode dir, EncryptionMode encryptionMode) {
|
||||||
|
this(encryptionMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use constructor without fs parameter
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public EncryptionInfo(
|
public EncryptionInfo(
|
||||||
POIFSFileSystem fs
|
POIFSFileSystem fs
|
||||||
, EncryptionMode encryptionMode
|
, EncryptionMode encryptionMode
|
||||||
|
@ -99,10 +166,14 @@ public class EncryptionInfo {
|
||||||
, int keyBits
|
, int keyBits
|
||||||
, int blockSize
|
, int blockSize
|
||||||
, ChainingMode chainingMode
|
, 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(
|
public EncryptionInfo(
|
||||||
NPOIFSFileSystem fs
|
NPOIFSFileSystem fs
|
||||||
, EncryptionMode encryptionMode
|
, EncryptionMode encryptionMode
|
||||||
|
@ -111,10 +182,14 @@ public class EncryptionInfo {
|
||||||
, int keyBits
|
, int keyBits
|
||||||
, int blockSize
|
, int blockSize
|
||||||
, ChainingMode chainingMode
|
, 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(
|
public EncryptionInfo(
|
||||||
DirectoryNode dir
|
DirectoryNode dir
|
||||||
, EncryptionMode encryptionMode
|
, EncryptionMode encryptionMode
|
||||||
|
@ -123,7 +198,36 @@ public class EncryptionInfo {
|
||||||
, int keyBits
|
, int keyBits
|
||||||
, int blockSize
|
, int blockSize
|
||||||
, ChainingMode chainingMode
|
, 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;
|
versionMajor = encryptionMode.versionMajor;
|
||||||
versionMinor = encryptionMode.versionMinor;
|
versionMinor = encryptionMode.versionMinor;
|
||||||
encryptionFlags = encryptionMode.encryptionFlags;
|
encryptionFlags = encryptionMode.encryptionFlags;
|
||||||
|
|
|
@ -18,13 +18,36 @@ package org.apache.poi.poifs.crypt;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
import org.apache.poi.util.LittleEndianInput;
|
||||||
|
|
||||||
public interface EncryptionInfoBuilder {
|
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);
|
void initialize(EncryptionInfo ei, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the header data
|
||||||
|
*/
|
||||||
EncryptionHeader getHeader();
|
EncryptionHeader getHeader();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the verifier data
|
||||||
|
*/
|
||||||
EncryptionVerifier getVerifier();
|
EncryptionVerifier getVerifier();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the decryptor
|
||||||
|
*/
|
||||||
Decryptor getDecryptor();
|
Decryptor getDecryptor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the encryptor
|
||||||
|
*/
|
||||||
Encryptor getEncryptor();
|
Encryptor getEncryptor();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,24 @@
|
||||||
|
|
||||||
package org.apache.poi.poifs.crypt;
|
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 {
|
public enum EncryptionMode {
|
||||||
standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24)
|
/* @see <a href="http://msdn.microsoft.com/en-us/library/dd907466(v=office.12).aspx">2.3.6 Office Binary Document RC4 Encryption</a> */
|
||||||
, agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40);
|
binaryRC4("org.apache.poi.poifs.crypt.binaryrc4.BinaryRC4EncryptionInfoBuilder", 1, 1, 0x0),
|
||||||
|
/* @see <a href="http://msdn.microsoft.com/en-us/library/dd905225(v=office.12).aspx">2.3.5 Office Binary Document RC4 CryptoAPI Encryption</a> */
|
||||||
|
cryptoAPI("org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionInfoBuilder", 4, 2, 0x04),
|
||||||
|
/* @see <a href="http://msdn.microsoft.com/en-us/library/dd906097(v=office.12).aspx">2.3.4.5 \EncryptionInfo Stream (Standard Encryption)</a> */
|
||||||
|
standard("org.apache.poi.poifs.crypt.standard.StandardEncryptionInfoBuilder", 4, 2, 0x24),
|
||||||
|
/* @see <a href="http://msdn.microsoft.com/en-us/library/dd925810(v=office.12).aspx">2.3.4.10 \EncryptionInfo Stream (Agile Encryption)</a> */
|
||||||
|
agile("org.apache.poi.poifs.crypt.agile.AgileEncryptionInfoBuilder", 4, 4, 0x40)
|
||||||
|
;
|
||||||
|
|
||||||
public final String builder;
|
public final String builder;
|
||||||
public final int versionMajor;
|
public final int versionMajor;
|
||||||
|
|
|
@ -41,6 +41,7 @@ public abstract class EncryptionVerifier {
|
||||||
* The method name is misleading - you'll get the encrypted verifier, not the plain verifier
|
* The method name is misleading - you'll get the encrypted verifier, not the plain verifier
|
||||||
* @deprecated use getEncryptedVerifier()
|
* @deprecated use getEncryptedVerifier()
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public byte[] getVerifier() {
|
public byte[] getVerifier() {
|
||||||
return encryptedVerifier;
|
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
|
* The method name is misleading - you'll get the encrypted verifier hash, not the plain verifier hash
|
||||||
* @deprecated use getEnryptedVerifierHash
|
* @deprecated use getEnryptedVerifierHash
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public byte[] getVerifierHash() {
|
public byte[] getVerifierHash() {
|
||||||
return encryptedVerifierHash;
|
return encryptedVerifierHash;
|
||||||
}
|
}
|
||||||
|
@ -76,6 +78,7 @@ public abstract class EncryptionVerifier {
|
||||||
/**
|
/**
|
||||||
* @deprecated use getCipherAlgorithm().jceId
|
* @deprecated use getCipherAlgorithm().jceId
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public String getAlgorithmName() {
|
public String getAlgorithmName() {
|
||||||
return cipherAlgorithm.jceId;
|
return cipherAlgorithm.jceId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="http://msdn.microsoft.com/en-us/library/dd943321(v=office.12).aspx">2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream</a>
|
||||||
|
*/
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="http://msdn.microsoft.com/en-us/library/dd943321(v=office.12).aspx">2.3.5.4 RC4 CryptoAPI Encrypted Summary Stream</a>
|
||||||
|
*/
|
||||||
|
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<StreamDescriptorEntry> descList = new ArrayList<StreamDescriptorEntry>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ import org.apache.poi.poifs.crypt.ChainingMode;
|
||||||
import org.apache.poi.poifs.crypt.CryptoFunctions;
|
import org.apache.poi.poifs.crypt.CryptoFunctions;
|
||||||
import org.apache.poi.poifs.crypt.Decryptor;
|
import org.apache.poi.poifs.crypt.Decryptor;
|
||||||
import org.apache.poi.poifs.crypt.EncryptionHeader;
|
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.EncryptionVerifier;
|
||||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||||
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||||
|
@ -47,12 +47,12 @@ import org.apache.poi.util.LittleEndian;
|
||||||
public class StandardDecryptor extends Decryptor {
|
public class StandardDecryptor extends Decryptor {
|
||||||
private long _length = -1;
|
private long _length = -1;
|
||||||
|
|
||||||
protected StandardDecryptor(EncryptionInfo info) {
|
protected StandardDecryptor(EncryptionInfoBuilder builder) {
|
||||||
super(info);
|
super(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean verifyPassword(String password) {
|
public boolean verifyPassword(String password) {
|
||||||
EncryptionVerifier ver = info.getVerifier();
|
EncryptionVerifier ver = builder.getVerifier();
|
||||||
SecretKey skey = generateSecretKey(password, ver, getKeySizeInBytes());
|
SecretKey skey = generateSecretKey(password, ver, getKeySizeInBytes());
|
||||||
Cipher cipher = getCipher(skey);
|
Cipher cipher = getCipher(skey);
|
||||||
|
|
||||||
|
@ -64,7 +64,11 @@ public class StandardDecryptor extends Decryptor {
|
||||||
byte[] calcVerifierHash = sha1.digest(verifier);
|
byte[] calcVerifierHash = sha1.digest(verifier);
|
||||||
byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();
|
byte encryptedVerifierHash[] = ver.getEncryptedVerifierHash();
|
||||||
byte decryptedVerifierHash[] = cipher.doFinal(encryptedVerifierHash);
|
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)) {
|
if (Arrays.equals(calcVerifierHash, verifierHash)) {
|
||||||
setSecretKey(skey);
|
setSecretKey(skey);
|
||||||
|
@ -93,7 +97,7 @@ public class StandardDecryptor extends Decryptor {
|
||||||
System.arraycopy(x1, 0, x3, 0, x1.length);
|
System.arraycopy(x1, 0, x3, 0, x1.length);
|
||||||
System.arraycopy(x2, 0, x3, x1.length, x2.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);
|
SecretKey skey = new SecretKeySpec(key, ver.getCipherAlgorithm().jceId);
|
||||||
return skey;
|
return skey;
|
||||||
|
@ -111,24 +115,8 @@ public class StandardDecryptor extends Decryptor {
|
||||||
return sha1.digest(buff);
|
return sha1.digest(buff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a byte array of the requested length,
|
|
||||||
* truncated or zero padded as needed.
|
|
||||||
* Behaves like Arrays.copyOf in Java 1.6
|
|
||||||
*/
|
|
||||||
protected static byte[] truncateOrPad(byte[] source, int length) {
|
|
||||||
byte[] result = new byte[length];
|
|
||||||
System.arraycopy(source, 0, result, 0, Math.min(length, source.length));
|
|
||||||
if(length > source.length) {
|
|
||||||
for(int i=source.length; i<length; i++) {
|
|
||||||
result[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Cipher getCipher(SecretKey key) {
|
private Cipher getCipher(SecretKey key) {
|
||||||
EncryptionHeader em = info.getHeader();
|
EncryptionHeader em = builder.getHeader();
|
||||||
ChainingMode cm = em.getChainingMode();
|
ChainingMode cm = em.getChainingMode();
|
||||||
assert(cm == ChainingMode.ecb);
|
assert(cm == ChainingMode.ecb);
|
||||||
return CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), cm, null, Cipher.DECRYPT_MODE);
|
return CryptoFunctions.getCipher(key, em.getCipherAlgorithm(), cm, null, Cipher.DECRYPT_MODE);
|
||||||
|
@ -142,7 +130,7 @@ public class StandardDecryptor extends Decryptor {
|
||||||
// limit wrong calculated ole entries - (bug #57080)
|
// limit wrong calculated ole entries - (bug #57080)
|
||||||
// standard encryption always uses aes encoding, so blockSize is always 16
|
// standard encryption always uses aes encoding, so blockSize is always 16
|
||||||
// http://stackoverflow.com/questions/3283787/size-of-data-after-aes-encryption
|
// http://stackoverflow.com/questions/3283787/size-of-data-after-aes-encryption
|
||||||
int blockSize = info.getHeader().getCipherAlgorithm().blockSize;
|
int blockSize = builder.getHeader().getCipherAlgorithm().blockSize;
|
||||||
long cipherLen = (_length/blockSize + 1) * blockSize;
|
long cipherLen = (_length/blockSize + 1) * blockSize;
|
||||||
Cipher cipher = getCipher(getSecretKey());
|
Cipher cipher = getCipher(getSecretKey());
|
||||||
|
|
||||||
|
@ -150,12 +138,11 @@ public class StandardDecryptor extends Decryptor {
|
||||||
return new BoundedInputStream(new CipherInputStream(boundedDis, cipher), _length);
|
return new BoundedInputStream(new CipherInputStream(boundedDis, cipher), _length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the length of the stream returned by {@link #getDataStream(DirectoryNode)}
|
||||||
|
*/
|
||||||
public long getLength(){
|
public long getLength(){
|
||||||
if(_length == -1) throw new IllegalStateException("Decryptor.getDataStream() was not called");
|
if(_length == -1) throw new IllegalStateException("Decryptor.getDataStream() was not called");
|
||||||
return _length;
|
return _length;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getKeySizeInBytes() {
|
|
||||||
return info.getHeader().getKeySize()/8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,45 +17,37 @@
|
||||||
package org.apache.poi.poifs.crypt.standard;
|
package org.apache.poi.poifs.crypt.standard;
|
||||||
|
|
||||||
import static org.apache.poi.poifs.crypt.CryptoFunctions.getUtf16LeString;
|
import static org.apache.poi.poifs.crypt.CryptoFunctions.getUtf16LeString;
|
||||||
|
import static org.apache.poi.poifs.crypt.EncryptionInfo.flagAES;
|
||||||
|
import static org.apache.poi.poifs.crypt.EncryptionInfo.flagCryptoAPI;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
import org.apache.poi.poifs.crypt.ChainingMode;
|
import org.apache.poi.poifs.crypt.ChainingMode;
|
||||||
import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
||||||
import org.apache.poi.poifs.crypt.CipherProvider;
|
import org.apache.poi.poifs.crypt.CipherProvider;
|
||||||
import org.apache.poi.poifs.crypt.EncryptionHeader;
|
import org.apache.poi.poifs.crypt.EncryptionHeader;
|
||||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||||
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
|
||||||
import org.apache.poi.util.BitField;
|
|
||||||
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
||||||
import org.apache.poi.util.LittleEndianConsts;
|
import org.apache.poi.util.LittleEndianConsts;
|
||||||
|
import org.apache.poi.util.LittleEndianInput;
|
||||||
import org.apache.poi.util.LittleEndianOutput;
|
import org.apache.poi.util.LittleEndianOutput;
|
||||||
|
|
||||||
public class StandardEncryptionHeader extends EncryptionHeader implements EncryptionRecord {
|
public class StandardEncryptionHeader extends EncryptionHeader implements EncryptionRecord {
|
||||||
// A flag that specifies whether CryptoAPI RC4 or ECMA-376 encryption
|
|
||||||
// [ECMA-376] is used. It MUST be 1 unless fExternal is 1. If fExternal is 1, it MUST be 0.
|
|
||||||
private static BitField flagsCryptoAPI = new BitField(0x04);
|
|
||||||
|
|
||||||
// A value that MUST be 0 if document properties are encrypted. The
|
protected StandardEncryptionHeader(LittleEndianInput is) throws IOException {
|
||||||
// encryption of document properties is specified in section 2.3.5.4 [MS-OFFCRYPTO].
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static BitField flagsDocProps = new BitField(0x08);
|
|
||||||
|
|
||||||
// A value that MUST be 1 if extensible encryption is used,. If this value is 1,
|
|
||||||
// the value of every other field in this structure MUST be 0.
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static BitField flagsExternal = new BitField(0x10);
|
|
||||||
|
|
||||||
// A value that MUST be 1 if the protected content is an ECMA-376 document
|
|
||||||
// [ECMA-376]. If the fAES bit is 1, the fCryptoAPI bit MUST also be 1.
|
|
||||||
private static BitField flagsAES = new BitField(0x20);
|
|
||||||
|
|
||||||
protected StandardEncryptionHeader(DocumentInputStream is) throws IOException {
|
|
||||||
setFlags(is.readInt());
|
setFlags(is.readInt());
|
||||||
setSizeExtra(is.readInt());
|
setSizeExtra(is.readInt());
|
||||||
setCipherAlgorithm(CipherAlgorithm.fromEcmaId(is.readInt()));
|
setCipherAlgorithm(CipherAlgorithm.fromEcmaId(is.readInt()));
|
||||||
setHashAlgorithm(HashAlgorithm.fromEcmaId(is.readInt()));
|
setHashAlgorithm(HashAlgorithm.fromEcmaId(is.readInt()));
|
||||||
setKeySize(is.readInt());
|
int keySize = is.readInt();
|
||||||
|
if (keySize == 0) {
|
||||||
|
// for the sake of inheritance of the cryptoAPI classes
|
||||||
|
// see 2.3.5.1 RC4 CryptoAPI Encryption Header
|
||||||
|
// If set to 0x00000000, it MUST be interpreted as 0x00000028 bits.
|
||||||
|
keySize = 0x28;
|
||||||
|
}
|
||||||
|
setKeySize(keySize);
|
||||||
setBlockSize(getKeySize());
|
setBlockSize(getKeySize());
|
||||||
setCipherProvider(CipherProvider.fromEcmaId(is.readInt()));
|
setCipherProvider(CipherProvider.fromEcmaId(is.readInt()));
|
||||||
|
|
||||||
|
@ -63,9 +55,9 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp
|
||||||
|
|
||||||
// CSPName may not always be specified
|
// CSPName may not always be specified
|
||||||
// In some cases, the salt value of the EncryptionVerifier is the next chunk of data
|
// In some cases, the salt value of the EncryptionVerifier is the next chunk of data
|
||||||
is.mark(LittleEndianConsts.INT_SIZE+1);
|
((InputStream)is).mark(LittleEndianConsts.INT_SIZE+1);
|
||||||
int checkForSalt = is.readInt();
|
int checkForSalt = is.readInt();
|
||||||
is.reset();
|
((InputStream)is).reset();
|
||||||
|
|
||||||
if (checkForSalt == 16) {
|
if (checkForSalt == 16) {
|
||||||
setCspName("");
|
setCspName("");
|
||||||
|
@ -89,12 +81,15 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp
|
||||||
setKeySize(keyBits);
|
setKeySize(keyBits);
|
||||||
setBlockSize(blockSize);
|
setBlockSize(blockSize);
|
||||||
setCipherProvider(cipherAlgorithm.provider);
|
setCipherProvider(cipherAlgorithm.provider);
|
||||||
setFlags(flagsCryptoAPI.setBoolean(0, true)
|
setFlags(flagCryptoAPI.setBoolean(0, true)
|
||||||
| flagsAES.setBoolean(0, cipherAlgorithm.provider == CipherProvider.aes));
|
| flagAES.setBoolean(0, cipherAlgorithm.provider == CipherProvider.aes));
|
||||||
// see http://msdn.microsoft.com/en-us/library/windows/desktop/bb931357(v=vs.85).aspx for a full list
|
// see http://msdn.microsoft.com/en-us/library/windows/desktop/bb931357(v=vs.85).aspx for a full list
|
||||||
// setCspName("Microsoft Enhanced RSA and AES Cryptographic Provider");
|
// setCspName("Microsoft Enhanced RSA and AES Cryptographic Provider");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* serializes the header
|
||||||
|
*/
|
||||||
public void write(LittleEndianByteArrayOutputStream bos) {
|
public void write(LittleEndianByteArrayOutputStream bos) {
|
||||||
int startIdx = bos.getWriteIndex();
|
int startIdx = bos.getWriteIndex();
|
||||||
LittleEndianOutput sizeOutput = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE);
|
LittleEndianOutput sizeOutput = bos.createDelayedOutput(LittleEndianConsts.INT_SIZE);
|
||||||
|
@ -106,10 +101,10 @@ public class StandardEncryptionHeader extends EncryptionHeader implements Encryp
|
||||||
bos.writeInt(getCipherProvider().ecmaId);
|
bos.writeInt(getCipherProvider().ecmaId);
|
||||||
bos.writeInt(0); // reserved1
|
bos.writeInt(0); // reserved1
|
||||||
bos.writeInt(0); // reserved2
|
bos.writeInt(0); // reserved2
|
||||||
if (getCspName() != null) {
|
String cspName = getCspName();
|
||||||
bos.write(getUtf16LeString(getCspName()));
|
if (cspName == null) cspName = getCipherProvider().cipherProviderName;
|
||||||
bos.writeShort(0);
|
bos.write(getUtf16LeString(cspName));
|
||||||
}
|
bos.writeShort(0);
|
||||||
int headerSize = bos.getWriteIndex()-startIdx-LittleEndianConsts.INT_SIZE;
|
int headerSize = bos.getWriteIndex()-startIdx-LittleEndianConsts.INT_SIZE;
|
||||||
sizeOutput.writeInt(headerSize);
|
sizeOutput.writeInt(headerSize);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
||||||
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||||
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
|
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
|
||||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||||
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
import org.apache.poi.util.LittleEndianInput;
|
||||||
|
|
||||||
public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
|
public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
|
||||||
|
|
||||||
|
@ -34,7 +34,10 @@ public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
|
||||||
StandardDecryptor decryptor;
|
StandardDecryptor decryptor;
|
||||||
StandardEncryptor encryptor;
|
StandardEncryptor encryptor;
|
||||||
|
|
||||||
public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {
|
/**
|
||||||
|
* initialize the builder from a stream
|
||||||
|
*/
|
||||||
|
public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException {
|
||||||
this.info = info;
|
this.info = info;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
|
@ -43,10 +46,13 @@ public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
|
||||||
verifier = new StandardEncryptionVerifier(dis, header);
|
verifier = new StandardEncryptionVerifier(dis, header);
|
||||||
|
|
||||||
if (info.getVersionMinor() == 2 && (info.getVersionMajor() == 3 || info.getVersionMajor() == 4)) {
|
if (info.getVersionMinor() == 2 && (info.getVersionMajor() == 3 || info.getVersionMajor() == 4)) {
|
||||||
decryptor = new StandardDecryptor(info);
|
decryptor = new StandardDecryptor(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initialize the builder from scratch
|
||||||
|
*/
|
||||||
public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
|
public void initialize(EncryptionInfo info, CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
|
||||||
this.info = info;
|
this.info = info;
|
||||||
|
|
||||||
|
@ -80,7 +86,7 @@ public class StandardEncryptionInfoBuilder implements EncryptionInfoBuilder {
|
||||||
}
|
}
|
||||||
header = new StandardEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
|
header = new StandardEncryptionHeader(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
|
||||||
verifier = new StandardEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
|
verifier = new StandardEncryptionVerifier(cipherAlgorithm, hashAlgorithm, keyBits, blockSize, chainingMode);
|
||||||
decryptor = new StandardDecryptor(info);
|
decryptor = new StandardDecryptor(this);
|
||||||
encryptor = new StandardEncryptor(this);
|
encryptor = new StandardEncryptor(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,8 @@ import org.apache.poi.poifs.crypt.ChainingMode;
|
||||||
import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
||||||
import org.apache.poi.poifs.crypt.EncryptionVerifier;
|
import org.apache.poi.poifs.crypt.EncryptionVerifier;
|
||||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||||
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
|
||||||
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
||||||
|
import org.apache.poi.util.LittleEndianInput;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used when checking if a key is valid for a document
|
* Used when checking if a key is valid for a document
|
||||||
|
@ -31,7 +31,7 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En
|
||||||
private static final int SPIN_COUNT = 50000;
|
private static final int SPIN_COUNT = 50000;
|
||||||
private final int verifierHashSize;
|
private final int verifierHashSize;
|
||||||
|
|
||||||
protected StandardEncryptionVerifier(DocumentInputStream is, StandardEncryptionHeader header) {
|
protected StandardEncryptionVerifier(LittleEndianInput is, StandardEncryptionHeader header) {
|
||||||
int saltSize = is.readInt();
|
int saltSize = is.readInt();
|
||||||
|
|
||||||
if (saltSize!=16) {
|
if (saltSize!=16) {
|
||||||
|
@ -53,10 +53,10 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En
|
||||||
setEncryptedVerifierHash(encryptedVerifierHash);
|
setEncryptedVerifierHash(encryptedVerifierHash);
|
||||||
|
|
||||||
setSpinCount(SPIN_COUNT);
|
setSpinCount(SPIN_COUNT);
|
||||||
setCipherAlgorithm(CipherAlgorithm.aes128);
|
setCipherAlgorithm(header.getCipherAlgorithm());
|
||||||
setChainingMode(ChainingMode.ecb);
|
setChainingMode(header.getChainingMode());
|
||||||
setEncryptedKey(null);
|
setEncryptedKey(null);
|
||||||
setHashAlgorithm(HashAlgorithm.sha1);
|
setHashAlgorithm(header.getHashAlgorithmEx());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected StandardEncryptionVerifier(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
|
protected StandardEncryptionVerifier(CipherAlgorithm cipherAlgorithm, HashAlgorithm hashAlgorithm, int keyBits, int blockSize, ChainingMode chainingMode) {
|
||||||
|
@ -97,12 +97,18 @@ public class StandardEncryptionVerifier extends EncryptionVerifier implements En
|
||||||
assert(encryptedVerifier.length == 16);
|
assert(encryptedVerifier.length == 16);
|
||||||
bos.write(encryptedVerifier);
|
bos.write(encryptedVerifier);
|
||||||
|
|
||||||
// The number of bytes used by the encrypted Verifier hash MUST be 32.
|
|
||||||
// The number of bytes used by the decrypted Verifier hash is given by
|
// The number of bytes used by the decrypted Verifier hash is given by
|
||||||
// the VerifierHashSize field, which MUST be 20
|
// the VerifierHashSize field, which MUST be 20
|
||||||
byte encryptedVerifierHash[] = getEncryptedVerifierHash();
|
|
||||||
assert(encryptedVerifierHash.length == 32);
|
|
||||||
bos.writeInt(20);
|
bos.writeInt(20);
|
||||||
|
|
||||||
|
// EncryptedVerifierHash: An array of bytes that contains the encrypted form of the hash of
|
||||||
|
// the randomly generated Verifier value. The length of the array MUST be the size of the
|
||||||
|
// encryption block size multiplied by the number of blocks needed to encrypt the hash of the
|
||||||
|
// Verifier. If the encryption algorithm is RC4, the length MUST be 20 bytes. If the encryption
|
||||||
|
// algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash
|
||||||
|
// field, only the first VerifierHashSize bytes MUST be used.
|
||||||
|
byte encryptedVerifierHash[] = getEncryptedVerifierHash();
|
||||||
|
assert(encryptedVerifierHash.length == getCipherAlgorithm().encryptedVerifierHashLength);
|
||||||
bos.write(encryptedVerifierHash);
|
bos.write(encryptedVerifierHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.apache.poi.poifs.crypt.standard;
|
||||||
|
|
||||||
import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry;
|
import static org.apache.poi.poifs.crypt.DataSpaceMapUtils.createEncryptionEntry;
|
||||||
import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.generateSecretKey;
|
import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.generateSecretKey;
|
||||||
import static org.apache.poi.poifs.crypt.standard.StandardDecryptor.truncateOrPad;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
@ -30,6 +29,7 @@ import java.io.OutputStream;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
@ -96,7 +96,7 @@ public class StandardEncryptor extends Encryptor {
|
||||||
// algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash
|
// algorithm is AES, the length MUST be 32 bytes. After decrypting the EncryptedVerifierHash
|
||||||
// field, only the first VerifierHashSize bytes MUST be used.
|
// field, only the first VerifierHashSize bytes MUST be used.
|
||||||
int encVerHashSize = ver.getCipherAlgorithm().encryptedVerifierHashLength;
|
int encVerHashSize = ver.getCipherAlgorithm().encryptedVerifierHashLength;
|
||||||
byte encryptedVerifierHash[] = cipher.doFinal(truncateOrPad(calcVerifierHash, encVerHashSize));
|
byte encryptedVerifierHash[] = cipher.doFinal(Arrays.copyOf(calcVerifierHash, encVerHashSize));
|
||||||
|
|
||||||
ver.setEncryptedVerifier(encryptedVerifier);
|
ver.setEncryptedVerifier(encryptedVerifier);
|
||||||
ver.setEncryptedVerifierHash(encryptedVerifierHash);
|
ver.setEncryptedVerifierHash(encryptedVerifierHash);
|
||||||
|
|
|
@ -166,4 +166,9 @@ public class DocumentInputStream extends InputStream implements LittleEndianInpu
|
||||||
public int readUByte() {
|
public int readUByte() {
|
||||||
return delegate.readUByte();
|
return delegate.readUByte();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long readUInt() {
|
||||||
|
int i = readInt();
|
||||||
|
return i & 0xFFFFFFFFL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import java.io.FilterInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import org.apache.poi.util.LittleEndian.BufferUnderrunException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps an {@link InputStream} providing {@link LittleEndianInput}<p/>
|
* Wraps an {@link InputStream} providing {@link LittleEndianInput}<p/>
|
||||||
*
|
*
|
||||||
|
@ -33,6 +35,7 @@ public class LittleEndianInputStream extends FilterInputStream implements Little
|
||||||
public LittleEndianInputStream(InputStream is) {
|
public LittleEndianInputStream(InputStream is) {
|
||||||
super(is);
|
super(is);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int available() {
|
public int available() {
|
||||||
try {
|
try {
|
||||||
return super.available();
|
return super.available();
|
||||||
|
@ -40,86 +43,75 @@ public class LittleEndianInputStream extends FilterInputStream implements Little
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte readByte() {
|
public byte readByte() {
|
||||||
return (byte)readUByte();
|
return (byte)readUByte();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int readUByte() {
|
public int readUByte() {
|
||||||
int ch;
|
byte buf[] = new byte[1];
|
||||||
try {
|
try {
|
||||||
ch = in.read();
|
checkEOF(read(buf), 1);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
checkEOF(ch);
|
return LittleEndian.getUByte(buf);
|
||||||
return ch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public double readDouble() {
|
public double readDouble() {
|
||||||
return Double.longBitsToDouble(readLong());
|
return Double.longBitsToDouble(readLong());
|
||||||
}
|
}
|
||||||
|
|
||||||
public int readInt() {
|
public int readInt() {
|
||||||
int ch1;
|
byte buf[] = new byte[LittleEndianConsts.INT_SIZE];
|
||||||
int ch2;
|
|
||||||
int ch3;
|
|
||||||
int ch4;
|
|
||||||
try {
|
try {
|
||||||
ch1 = in.read();
|
checkEOF(read(buf), buf.length);
|
||||||
ch2 = in.read();
|
|
||||||
ch3 = in.read();
|
|
||||||
ch4 = in.read();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
checkEOF(ch1 | ch2 | ch3 | ch4);
|
return LittleEndian.getInt(buf);
|
||||||
return (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
public long readLong() {
|
||||||
int b0;
|
byte buf[] = new byte[LittleEndianConsts.LONG_SIZE];
|
||||||
int b1;
|
|
||||||
int b2;
|
|
||||||
int b3;
|
|
||||||
int b4;
|
|
||||||
int b5;
|
|
||||||
int b6;
|
|
||||||
int b7;
|
|
||||||
try {
|
try {
|
||||||
b0 = in.read();
|
checkEOF(read(buf), LittleEndianConsts.LONG_SIZE);
|
||||||
b1 = in.read();
|
|
||||||
b2 = in.read();
|
|
||||||
b3 = in.read();
|
|
||||||
b4 = in.read();
|
|
||||||
b5 = in.read();
|
|
||||||
b6 = in.read();
|
|
||||||
b7 = in.read();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
checkEOF(b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7);
|
return LittleEndian.getLong(buf);
|
||||||
return (((long)b7 << 56) +
|
|
||||||
((long)b6 << 48) +
|
|
||||||
((long)b5 << 40) +
|
|
||||||
((long)b4 << 32) +
|
|
||||||
((long)b3 << 24) +
|
|
||||||
(b2 << 16) +
|
|
||||||
(b1 << 8) +
|
|
||||||
(b0 << 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public short readShort() {
|
public short readShort() {
|
||||||
return (short)readUShort();
|
return (short)readUShort();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int readUShort() {
|
public int readUShort() {
|
||||||
int ch1;
|
byte buf[] = new byte[LittleEndianConsts.SHORT_SIZE];
|
||||||
int ch2;
|
|
||||||
try {
|
try {
|
||||||
ch1 = in.read();
|
checkEOF(read(buf), LittleEndianConsts.SHORT_SIZE);
|
||||||
ch2 = in.read();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
checkEOF(ch1 | ch2);
|
return LittleEndian.getUShort(buf);
|
||||||
return (ch2 << 8) + (ch1 << 0);
|
|
||||||
}
|
}
|
||||||
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");
|
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) {
|
public void readFully(byte[] buf, int off, int len) {
|
||||||
int max = off+len;
|
try {
|
||||||
for(int i=off; i<max; i++) {
|
checkEOF(read(buf, off, len), len);
|
||||||
int ch;
|
} catch (IOException e) {
|
||||||
try {
|
throw new RuntimeException(e);
|
||||||
ch = in.read();
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
checkEOF(ch);
|
|
||||||
buf[i] = (byte) ch;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,10 +40,12 @@ import javax.crypto.spec.RC2ParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
import org.apache.poi.EncryptedDocumentException;
|
import org.apache.poi.EncryptedDocumentException;
|
||||||
|
import org.apache.poi.poifs.crypt.ChunkedCipherInputStream;
|
||||||
import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
||||||
import org.apache.poi.poifs.crypt.CryptoFunctions;
|
import org.apache.poi.poifs.crypt.CryptoFunctions;
|
||||||
import org.apache.poi.poifs.crypt.Decryptor;
|
import org.apache.poi.poifs.crypt.Decryptor;
|
||||||
import org.apache.poi.poifs.crypt.EncryptionHeader;
|
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.EncryptionVerifier;
|
||||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||||
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry;
|
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry;
|
||||||
|
@ -55,9 +57,6 @@ import org.apache.poi.util.LittleEndian;
|
||||||
* Decryptor implementation for Agile Encryption
|
* Decryptor implementation for Agile Encryption
|
||||||
*/
|
*/
|
||||||
public class AgileDecryptor extends Decryptor {
|
public class AgileDecryptor extends Decryptor {
|
||||||
private final AgileEncryptionInfoBuilder builder;
|
|
||||||
|
|
||||||
|
|
||||||
private long _length = -1;
|
private long _length = -1;
|
||||||
|
|
||||||
protected static final byte[] kVerifierInputBlock;
|
protected static final byte[] kVerifierInputBlock;
|
||||||
|
@ -85,16 +84,15 @@ public class AgileDecryptor extends Decryptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AgileDecryptor(AgileEncryptionInfoBuilder builder) {
|
protected AgileDecryptor(AgileEncryptionInfoBuilder builder) {
|
||||||
super(builder.getInfo());
|
super(builder);
|
||||||
this.builder = builder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set decryption password
|
* set decryption password
|
||||||
*/
|
*/
|
||||||
public boolean verifyPassword(String password) throws GeneralSecurityException {
|
public boolean verifyPassword(String password) throws GeneralSecurityException {
|
||||||
AgileEncryptionVerifier ver = builder.getVerifier();
|
AgileEncryptionVerifier ver = (AgileEncryptionVerifier)builder.getVerifier();
|
||||||
AgileEncryptionHeader header = builder.getHeader();
|
AgileEncryptionHeader header = (AgileEncryptionHeader)builder.getHeader();
|
||||||
HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
|
HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
|
||||||
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
|
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
|
||||||
int blockSize = header.getBlockSize();
|
int blockSize = header.getBlockSize();
|
||||||
|
@ -206,8 +204,8 @@ public class AgileDecryptor extends Decryptor {
|
||||||
* @throws GeneralSecurityException
|
* @throws GeneralSecurityException
|
||||||
*/
|
*/
|
||||||
public boolean verifyPassword(KeyPair keyPair, X509Certificate x509) throws GeneralSecurityException {
|
public boolean verifyPassword(KeyPair keyPair, X509Certificate x509) throws GeneralSecurityException {
|
||||||
AgileEncryptionVerifier ver = builder.getVerifier();
|
AgileEncryptionVerifier ver = (AgileEncryptionVerifier)builder.getVerifier();
|
||||||
AgileEncryptionHeader header = builder.getHeader();
|
AgileEncryptionHeader header = (AgileEncryptionHeader)builder.getHeader();
|
||||||
HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
|
HashAlgorithm hashAlgo = header.getHashAlgorithmEx();
|
||||||
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
|
CipherAlgorithm cipherAlgo = header.getCipherAlgorithm();
|
||||||
int blockSize = header.getBlockSize();
|
int blockSize = header.getBlockSize();
|
||||||
|
@ -257,10 +255,11 @@ public class AgileDecryptor extends Decryptor {
|
||||||
return fillSize;
|
return fillSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static byte[] hashInput(AgileEncryptionInfoBuilder builder, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) {
|
protected static byte[] hashInput(EncryptionInfoBuilder builder, byte pwHash[], byte blockKey[], byte inputKey[], int cipherMode) {
|
||||||
EncryptionVerifier ver = builder.getVerifier();
|
EncryptionVerifier ver = builder.getVerifier();
|
||||||
int keySize = builder.getDecryptor().getKeySizeInBytes();
|
AgileDecryptor dec = (AgileDecryptor)builder.getDecryptor();
|
||||||
int blockSize = builder.getDecryptor().getBlockSizeInBytes();
|
int keySize = dec.getKeySizeInBytes();
|
||||||
|
int blockSize = dec.getBlockSizeInBytes();
|
||||||
HashAlgorithm hashAlgo = ver.getHashAlgorithm();
|
HashAlgorithm hashAlgo = ver.getHashAlgorithm();
|
||||||
byte[] salt = ver.getSalt();
|
byte[] salt = ver.getSalt();
|
||||||
|
|
||||||
|
@ -283,7 +282,7 @@ public class AgileDecryptor extends Decryptor {
|
||||||
DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
|
DocumentInputStream dis = dir.createDocumentInputStream("EncryptedPackage");
|
||||||
_length = dis.readLong();
|
_length = dis.readLong();
|
||||||
|
|
||||||
ChunkedCipherInputStream cipherStream = new ChunkedCipherInputStream(dis, _length);
|
ChunkedCipherInputStream cipherStream = new AgileCipherInputStream(dis, _length);
|
||||||
return cipherStream;
|
return cipherStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,6 +291,31 @@ public class AgileDecryptor extends Decryptor {
|
||||||
return _length;
|
return _length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected static Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk, EncryptionInfoBuilder builder, SecretKey skey, int encryptionMode)
|
||||||
|
throws GeneralSecurityException {
|
||||||
|
EncryptionHeader header = builder.getHeader();
|
||||||
|
if (existing == null || lastChunk) {
|
||||||
|
String padding = (lastChunk ? "PKCS5Padding" : "NoPadding");
|
||||||
|
existing = getCipher(skey, header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), encryptionMode, padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] blockKey = new byte[4];
|
||||||
|
LittleEndian.putInt(blockKey, 0, block);
|
||||||
|
byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, header.getBlockSize());
|
||||||
|
|
||||||
|
AlgorithmParameterSpec aps;
|
||||||
|
if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {
|
||||||
|
aps = new RC2ParameterSpec(skey.getEncoded().length*8, iv);
|
||||||
|
} else {
|
||||||
|
aps = new IvParameterSpec(iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.init(encryptionMode, skey, aps);
|
||||||
|
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2.3.4.15 Data Encryption (Agile Encryption)
|
* 2.3.4.15 Data Encryption (Agile Encryption)
|
||||||
*
|
*
|
||||||
|
@ -307,107 +331,18 @@ public class AgileDecryptor extends Decryptor {
|
||||||
* that the StreamSize field of the EncryptedPackage field specifies the number of bytes of
|
* that the StreamSize field of the EncryptedPackage field specifies the number of bytes of
|
||||||
* unencrypted data as specified in section 2.3.4.4.
|
* unencrypted data as specified in section 2.3.4.4.
|
||||||
*/
|
*/
|
||||||
private class ChunkedCipherInputStream extends InputStream {
|
private class AgileCipherInputStream extends ChunkedCipherInputStream {
|
||||||
private int _lastIndex = 0;
|
public AgileCipherInputStream(DocumentInputStream stream, long size)
|
||||||
private long _pos = 0;
|
throws GeneralSecurityException {
|
||||||
private final long _size;
|
super(stream, size, 4096);
|
||||||
private final InputStream _stream;
|
|
||||||
private byte[] _chunk;
|
|
||||||
private Cipher _cipher;
|
|
||||||
|
|
||||||
public ChunkedCipherInputStream(DocumentInputStream stream, long size)
|
|
||||||
throws GeneralSecurityException {
|
|
||||||
EncryptionHeader header = info.getHeader();
|
|
||||||
_size = size;
|
|
||||||
_stream = stream;
|
|
||||||
_cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), header.getKeySalt(), Cipher.DECRYPT_MODE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int read() throws IOException {
|
// TODO: calculate integrity hmac while reading the stream
|
||||||
byte[] b = new byte[1];
|
// for a post-validation of the data
|
||||||
if (read(b) == 1)
|
|
||||||
return b[0];
|
protected Cipher initCipherForBlock(Cipher cipher, int block)
|
||||||
return -1;
|
throws GeneralSecurityException {
|
||||||
|
return AgileDecryptor.initCipherForBlock(cipher, block, false, builder, getSecretKey(), Cipher.DECRYPT_MODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int read(byte[] b) throws IOException {
|
|
||||||
return read(b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int read(byte[] b, int off, int len) throws IOException {
|
|
||||||
int total = 0;
|
|
||||||
|
|
||||||
if (available() <= 0) return -1;
|
|
||||||
|
|
||||||
while (len > 0) {
|
|
||||||
if (_chunk == null) {
|
|
||||||
try {
|
|
||||||
_chunk = nextChunk();
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
throw new EncryptedDocumentException(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int count = (int)(4096L - (_pos & 0xfff));
|
|
||||||
int avail = available();
|
|
||||||
if (avail == 0) {
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
count = Math.min(avail, Math.min(count, len));
|
|
||||||
System.arraycopy(_chunk, (int)(_pos & 0xfff), b, off, count);
|
|
||||||
off += count;
|
|
||||||
len -= count;
|
|
||||||
_pos += count;
|
|
||||||
if ((_pos & 0xfff) == 0)
|
|
||||||
_chunk = null;
|
|
||||||
total += count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long skip(long n) throws IOException {
|
|
||||||
long start = _pos;
|
|
||||||
long skip = Math.min(available(), n);
|
|
||||||
|
|
||||||
if ((((_pos + skip) ^ start) & ~0xfff) != 0)
|
|
||||||
_chunk = null;
|
|
||||||
_pos += skip;
|
|
||||||
return skip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int available() throws IOException { return (int)(_size - _pos); }
|
|
||||||
public void close() throws IOException { _stream.close(); }
|
|
||||||
public boolean markSupported() { return false; }
|
|
||||||
|
|
||||||
private byte[] nextChunk() throws GeneralSecurityException, IOException {
|
|
||||||
int index = (int)(_pos >> 12);
|
|
||||||
byte[] blockKey = new byte[4];
|
|
||||||
LittleEndian.putInt(blockKey, 0, index);
|
|
||||||
EncryptionHeader header = info.getHeader();
|
|
||||||
byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, getBlockSizeInBytes());
|
|
||||||
AlgorithmParameterSpec aps;
|
|
||||||
if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {
|
|
||||||
aps = new RC2ParameterSpec(getSecretKey().getEncoded().length*8, iv);
|
|
||||||
} else {
|
|
||||||
aps = new IvParameterSpec(iv);
|
|
||||||
}
|
|
||||||
|
|
||||||
_cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), aps);
|
|
||||||
if (_lastIndex != index)
|
|
||||||
_stream.skip((index - _lastIndex) << 12);
|
|
||||||
|
|
||||||
byte[] block = new byte[Math.min(_stream.available(), 4096)];
|
|
||||||
_stream.read(block);
|
|
||||||
_lastIndex = index + 1;
|
|
||||||
return _cipher.doFinal(block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getBlockSizeInBytes() {
|
|
||||||
return info.getHeader().getBlockSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getKeySizeInBytes() {
|
|
||||||
return info.getHeader().getKeySize()/8;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||||
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
|
import org.apache.poi.poifs.crypt.EncryptionInfoBuilder;
|
||||||
import org.apache.poi.poifs.crypt.EncryptionMode;
|
import org.apache.poi.poifs.crypt.EncryptionMode;
|
||||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
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 org.apache.xmlbeans.XmlException;
|
||||||
|
|
||||||
import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument;
|
import com.microsoft.schemas.office.x2006.encryption.EncryptionDocument;
|
||||||
|
@ -39,10 +39,10 @@ public class AgileEncryptionInfoBuilder implements EncryptionInfoBuilder {
|
||||||
AgileDecryptor decryptor;
|
AgileDecryptor decryptor;
|
||||||
AgileEncryptor encryptor;
|
AgileEncryptor encryptor;
|
||||||
|
|
||||||
public void initialize(EncryptionInfo info, DocumentInputStream dis) throws IOException {
|
public void initialize(EncryptionInfo info, LittleEndianInput dis) throws IOException {
|
||||||
this.info = info;
|
this.info = info;
|
||||||
|
|
||||||
EncryptionDocument ed = parseDescriptor(dis);
|
EncryptionDocument ed = parseDescriptor((InputStream)dis);
|
||||||
header = new AgileEncryptionHeader(ed);
|
header = new AgileEncryptionHeader(ed);
|
||||||
verifier = new AgileEncryptionVerifier(ed);
|
verifier = new AgileEncryptionVerifier(ed);
|
||||||
if (info.getVersionMajor() == EncryptionMode.agile.versionMajor
|
if (info.getVersionMajor() == EncryptionMode.agile.versionMajor
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
==================================================================== */
|
==================================================================== */
|
||||||
package org.apache.poi.poifs.crypt.agile;
|
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.getBlock0;
|
||||||
import static org.apache.poi.poifs.crypt.CryptoFunctions.getCipher;
|
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.getMessageDigest;
|
||||||
import static org.apache.poi.poifs.crypt.CryptoFunctions.hashPassword;
|
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.getNextBlockSize;
|
||||||
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.hashInput;
|
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.hashInput;
|
||||||
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kCryptoKeyBlock;
|
import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kCryptoKeyBlock;
|
||||||
|
@ -32,16 +32,12 @@ import static org.apache.poi.poifs.crypt.agile.AgileDecryptor.kVerifierInputBloc
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FilterOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.spec.AlgorithmParameterSpec;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
@ -49,28 +45,20 @@ import java.util.Random;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.RC2ParameterSpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
import org.apache.poi.EncryptedDocumentException;
|
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.CryptoFunctions;
|
||||||
import org.apache.poi.poifs.crypt.DataSpaceMapUtils;
|
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.EncryptionInfo;
|
||||||
import org.apache.poi.poifs.crypt.Encryptor;
|
import org.apache.poi.poifs.crypt.Encryptor;
|
||||||
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||||
import org.apache.poi.poifs.crypt.agile.AgileEncryptionVerifier.AgileCertificateEntry;
|
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.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.LittleEndian;
|
||||||
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
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 org.apache.xmlbeans.XmlOptions;
|
||||||
|
|
||||||
import com.microsoft.schemas.office.x2006.encryption.CTDataIntegrity;
|
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 {
|
public class AgileEncryptor extends Encryptor {
|
||||||
private final AgileEncryptionInfoBuilder builder;
|
private final AgileEncryptionInfoBuilder builder;
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private byte integritySalt[];
|
private byte integritySalt[];
|
||||||
private Mac integrityMD;
|
|
||||||
private byte pwHash[];
|
private byte pwHash[];
|
||||||
|
|
||||||
protected AgileEncryptor(AgileEncryptionInfoBuilder builder) {
|
protected AgileEncryptor(AgileEncryptionInfoBuilder builder) {
|
||||||
|
@ -214,10 +200,6 @@ public class AgileEncryptor extends Encryptor {
|
||||||
byte encryptedHmacKey[] = cipher.doFinal(filledSalt);
|
byte encryptedHmacKey[] = cipher.doFinal(filledSalt);
|
||||||
header.setEncryptedHmacKey(encryptedHmacKey);
|
header.setEncryptedHmacKey(encryptedHmacKey);
|
||||||
|
|
||||||
this.integrityMD = CryptoFunctions.getMac(hashAlgo);
|
|
||||||
this.integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId));
|
|
||||||
|
|
||||||
|
|
||||||
cipher = Cipher.getInstance("RSA");
|
cipher = Cipher.getInstance("RSA");
|
||||||
for (AgileCertificateEntry ace : ver.getCertificates()) {
|
for (AgileCertificateEntry ace : ver.getCertificates()) {
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, ace.x509.getPublicKey());
|
cipher.init(Cipher.ENCRYPT_MODE, ace.x509.getPublicKey());
|
||||||
|
@ -234,182 +216,59 @@ public class AgileEncryptor extends Encryptor {
|
||||||
public OutputStream getDataStream(DirectoryNode dir)
|
public OutputStream getDataStream(DirectoryNode dir)
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
// TODO: initialize headers
|
// TODO: initialize headers
|
||||||
OutputStream countStream = new ChunkedCipherOutputStream(dir);
|
AgileCipherOutputStream countStream = new AgileCipherOutputStream(dir);
|
||||||
return countStream;
|
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
|
* Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes:
|
||||||
* random access while allowing CBC modes to be used in the encryption process.
|
* 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33.
|
||||||
* 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
|
protected void updateIntegrityHMAC(File tmpFile, int oleStreamSize) throws GeneralSecurityException, IOException {
|
||||||
* section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer.
|
// as the integrity hmac needs to contain the StreamSize,
|
||||||
* Data blocks MUST then be encrypted by using the initialization vector and the intermediate key
|
// it's not possible to calculate it on-the-fly while buffering
|
||||||
* obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the
|
// TODO: add stream size parameter to getDataStream()
|
||||||
* KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to
|
|
||||||
* the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note
|
|
||||||
* that the StreamSize field of the EncryptedPackage field specifies the number of bytes of
|
|
||||||
* unencrypted data as specified in section 2.3.4.4.
|
|
||||||
*/
|
|
||||||
private class ChunkedCipherOutputStream extends FilterOutputStream implements POIFSWriterListener {
|
|
||||||
private long _pos = 0;
|
|
||||||
private final byte[] _chunk = new byte[4096];
|
|
||||||
private Cipher _cipher;
|
|
||||||
private final File fileOut;
|
|
||||||
protected final DirectoryNode dir;
|
|
||||||
|
|
||||||
public ChunkedCipherOutputStream(DirectoryNode dir) throws IOException {
|
|
||||||
super(null);
|
|
||||||
fileOut = TempFile.createTempFile("encrypted_package", "crypt");
|
|
||||||
this.out = new FileOutputStream(fileOut);
|
|
||||||
this.dir = dir;
|
|
||||||
EncryptionHeader header = builder.getHeader();
|
|
||||||
_cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(int b) throws IOException {
|
|
||||||
write(new byte[]{(byte)b});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(byte[] b) throws IOException {
|
|
||||||
write(b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(byte[] b, int off, int len)
|
|
||||||
throws IOException {
|
|
||||||
if (len == 0) return;
|
|
||||||
|
|
||||||
if (len < 0 || b.length < off+len) {
|
|
||||||
throw new IOException("not enough bytes in your input buffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
while (len > 0) {
|
|
||||||
int posInChunk = (int)(_pos & 0xfff);
|
|
||||||
int nextLen = Math.min(4096-posInChunk, len);
|
|
||||||
System.arraycopy(b, off, _chunk, posInChunk, nextLen);
|
|
||||||
_pos += nextLen;
|
|
||||||
off += nextLen;
|
|
||||||
len -= nextLen;
|
|
||||||
if ((_pos & 0xfff) == 0) {
|
|
||||||
writeChunk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeChunk() throws IOException {
|
|
||||||
EncryptionHeader header = builder.getHeader();
|
|
||||||
int blockSize = header.getBlockSize();
|
|
||||||
|
|
||||||
int posInChunk = (int)(_pos & 0xfff);
|
|
||||||
// normally posInChunk is 0, i.e. on the next chunk (-> index-1)
|
|
||||||
// but if called on close(), posInChunk is somewhere within the chunk data
|
|
||||||
int index = (int)(_pos >> 12);
|
|
||||||
if (posInChunk==0) {
|
|
||||||
index--;
|
|
||||||
posInChunk = 4096;
|
|
||||||
} else {
|
|
||||||
// pad the last chunk
|
|
||||||
_cipher = getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), null, Cipher.ENCRYPT_MODE, "PKCS5Padding");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] blockKey = new byte[4];
|
|
||||||
LittleEndian.putInt(blockKey, 0, index);
|
|
||||||
byte[] iv = generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), blockKey, blockSize);
|
|
||||||
try {
|
|
||||||
AlgorithmParameterSpec aps;
|
|
||||||
if (header.getCipherAlgorithm() == CipherAlgorithm.rc2) {
|
|
||||||
aps = new RC2ParameterSpec(getSecretKey().getEncoded().length*8, iv);
|
|
||||||
} else {
|
|
||||||
aps = new IvParameterSpec(iv);
|
|
||||||
}
|
|
||||||
|
|
||||||
_cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(), aps);
|
|
||||||
int ciLen = _cipher.doFinal(_chunk, 0, posInChunk, _chunk);
|
|
||||||
out.write(_chunk, 0, ciLen);
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
throw (IOException)new IOException().initCause(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() throws IOException {
|
|
||||||
writeChunk();
|
|
||||||
super.close();
|
|
||||||
writeToPOIFS();
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeToPOIFS() throws IOException {
|
|
||||||
DataSpaceMapUtils.addDefaultDataSpace(dir);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an HMAC, as specified in [RFC2104], of the encrypted form of the data (message),
|
|
||||||
* which the DataIntegrity element will verify by using the Salt generated in step 2 as the key.
|
|
||||||
* Note that the entire EncryptedPackage stream (1), including the StreamSize field, MUST be
|
|
||||||
* used as the message.
|
|
||||||
*
|
|
||||||
* Encrypt the HMAC as in step 3 by using a blockKey byte array consisting of the following bytes:
|
|
||||||
* 0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, and 0x33.
|
|
||||||
**/
|
|
||||||
byte buf[] = new byte[4096];
|
|
||||||
LittleEndian.putLong(buf, 0, _pos);
|
|
||||||
integrityMD.update(buf, 0, LittleEndianConsts.LONG_SIZE);
|
|
||||||
|
|
||||||
InputStream fis = new FileInputStream(fileOut);
|
|
||||||
for (int readBytes; (readBytes = fis.read(buf)) != -1; integrityMD.update(buf, 0, readBytes));
|
|
||||||
fis.close();
|
|
||||||
|
|
||||||
AgileEncryptionHeader header = builder.getHeader();
|
|
||||||
int blockSize = header.getBlockSize();
|
|
||||||
|
|
||||||
byte hmacValue[] = integrityMD.doFinal();
|
|
||||||
byte iv[] = CryptoFunctions.generateIv(header.getHashAlgorithmEx(), header.getKeySalt(), kIntegrityValueBlock, header.getBlockSize());
|
|
||||||
Cipher cipher = CryptoFunctions.getCipher(getSecretKey(), header.getCipherAlgorithm(), header.getChainingMode(), iv, Cipher.ENCRYPT_MODE);
|
|
||||||
try {
|
|
||||||
byte hmacValueFilled[] = getBlock0(hmacValue, getNextBlockSize(hmacValue.length, blockSize));
|
|
||||||
byte encryptedHmacValue[] = cipher.doFinal(hmacValueFilled);
|
|
||||||
header.setEncryptedHmacValue(encryptedHmacValue);
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
throw new EncryptedDocumentException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
createEncryptionInfoEntry(dir);
|
|
||||||
|
|
||||||
int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);
|
|
||||||
dir.createDocument("EncryptedPackage", oleStreamSize, this);
|
|
||||||
// TODO: any properties???
|
|
||||||
}
|
|
||||||
|
|
||||||
public void processPOIFSWriterEvent(POIFSWriterEvent event) {
|
|
||||||
try {
|
|
||||||
LittleEndianOutputStream leos = new LittleEndianOutputStream(event.getStream());
|
|
||||||
|
|
||||||
// StreamSize (8 bytes): An unsigned integer that specifies the number of bytes used by data
|
|
||||||
// encrypted within the EncryptedData field, not including the size of the StreamSize field.
|
|
||||||
// Note that the actual size of the \EncryptedPackage stream (1) can be larger than this
|
|
||||||
// value, depending on the block size of the chosen encryption algorithm
|
|
||||||
leos.writeLong(_pos);
|
|
||||||
|
|
||||||
FileInputStream fis = new FileInputStream(fileOut);
|
|
||||||
IOUtils.copy(fis, leos);
|
|
||||||
fis.close();
|
|
||||||
fileOut.delete();
|
|
||||||
|
|
||||||
leos.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new EncryptedDocumentException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void createEncryptionInfoEntry(DirectoryNode dir) throws IOException {
|
|
||||||
final CTKeyEncryptor.Uri.Enum passwordUri =
|
|
||||||
CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_PASSWORD;
|
|
||||||
final CTKeyEncryptor.Uri.Enum certificateUri =
|
|
||||||
CTKeyEncryptor.Uri.HTTP_SCHEMAS_MICROSOFT_COM_OFFICE_2006_KEY_ENCRYPTOR_CERTIFICATE;
|
|
||||||
|
|
||||||
AgileEncryptionVerifier ver = builder.getVerifier();
|
AgileEncryptionVerifier ver = builder.getVerifier();
|
||||||
|
HashAlgorithm hashAlgo = ver.getHashAlgorithm();
|
||||||
|
Mac integrityMD = CryptoFunctions.getMac(hashAlgo);
|
||||||
|
integrityMD.init(new SecretKeySpec(integritySalt, hashAlgo.jceHmacId));
|
||||||
|
|
||||||
|
byte buf[] = new byte[1024];
|
||||||
|
LittleEndian.putLong(buf, 0, oleStreamSize);
|
||||||
|
integrityMD.update(buf, 0, LittleEndian.LONG_SIZE);
|
||||||
|
|
||||||
|
FileInputStream fis = new FileInputStream(tmpFile);
|
||||||
|
int readBytes;
|
||||||
|
while ((readBytes = fis.read(buf)) != -1) {
|
||||||
|
integrityMD.update(buf, 0, readBytes);
|
||||||
|
}
|
||||||
|
fis.close();
|
||||||
|
|
||||||
|
byte hmacValue[] = integrityMD.doFinal();
|
||||||
|
|
||||||
AgileEncryptionHeader header = builder.getHeader();
|
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();
|
EncryptionDocument ed = EncryptionDocument.Factory.newInstance();
|
||||||
CTEncryption edRoot = ed.addNewEncryption();
|
CTEncryption edRoot = ed.addNewEncryption();
|
||||||
|
@ -485,6 +344,10 @@ public class AgileEncryptor extends Encryptor {
|
||||||
certData.setCertVerifier(ace.certVerifier);
|
certData.setCertVerifier(ace.certVerifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void marshallEncryptionDocument(EncryptionDocument ed, LittleEndianByteArrayOutputStream os) {
|
||||||
XmlOptions xo = new XmlOptions();
|
XmlOptions xo = new XmlOptions();
|
||||||
xo.setCharacterEncoding("UTF-8");
|
xo.setCharacterEncoding("UTF-8");
|
||||||
Map<String,String> nsMap = new HashMap<String,String>();
|
Map<String,String> nsMap = new HashMap<String,String>();
|
||||||
|
@ -494,33 +357,82 @@ public class AgileEncryptor extends Encryptor {
|
||||||
xo.setSaveSuggestedPrefixes(nsMap);
|
xo.setSaveSuggestedPrefixes(nsMap);
|
||||||
xo.setSaveNamespacesFirst();
|
xo.setSaveNamespacesFirst();
|
||||||
xo.setSaveAggressiveNamespaces();
|
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();
|
xo.setSaveNoXmlDecl();
|
||||||
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
bos.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".getBytes("UTF-8"));
|
try {
|
||||||
ed.save(bos, xo);
|
bos.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".getBytes("UTF-8"));
|
||||||
|
ed.save(bos, xo);
|
||||||
final byte buf[] = new byte[5000];
|
os.write(bos.toByteArray());
|
||||||
LittleEndianByteArrayOutputStream leos = new LittleEndianByteArrayOutputStream(buf, 0);
|
} catch (IOException e) {
|
||||||
EncryptionInfo info = builder.getInfo();
|
throw new EncryptedDocumentException("error marshalling encryption info document", e);
|
||||||
|
}
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)
|
||||||
|
throws IOException, GeneralSecurityException {
|
||||||
|
DataSpaceMapUtils.addDefaultDataSpace(dir);
|
||||||
|
|
||||||
|
final EncryptionInfo info = builder.getInfo();
|
||||||
|
|
||||||
|
EncryptionRecord er = new EncryptionRecord(){
|
||||||
|
public void write(LittleEndianByteArrayOutputStream bos) {
|
||||||
|
// EncryptionVersionInfo (4 bytes): A Version structure (section 2.1.4), where
|
||||||
|
// Version.vMajor MUST be 0x0004 and Version.vMinor MUST be 0x0004
|
||||||
|
bos.writeShort(info.getVersionMajor());
|
||||||
|
bos.writeShort(info.getVersionMinor());
|
||||||
|
// Reserved (4 bytes): A value that MUST be 0x00000040
|
||||||
|
bos.writeInt(info.getEncryptionFlags());
|
||||||
|
|
||||||
|
EncryptionDocument ed = createEncryptionDocument();
|
||||||
|
marshallEncryptionDocument(ed, bos);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createEncryptionEntry(dir, "EncryptionInfo", er);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2.3.4.15 Data Encryption (Agile Encryption)
|
||||||
|
*
|
||||||
|
* The EncryptedPackage stream (1) MUST be encrypted in 4096-byte segments to facilitate nearly
|
||||||
|
* random access while allowing CBC modes to be used in the encryption process.
|
||||||
|
* The initialization vector for the encryption process MUST be obtained by using the zero-based
|
||||||
|
* segment number as a blockKey and the binary form of the KeyData.saltValue as specified in
|
||||||
|
* section 2.3.4.12. The block number MUST be represented as a 32-bit unsigned integer.
|
||||||
|
* Data blocks MUST then be encrypted by using the initialization vector and the intermediate key
|
||||||
|
* obtained by decrypting the encryptedKeyValue from a KeyEncryptor contained within the
|
||||||
|
* KeyEncryptors sequence as specified in section 2.3.4.10. The final data block MUST be padded to
|
||||||
|
* the next integral multiple of the KeyData.blockSize value. Any padding bytes can be used. Note
|
||||||
|
* that the StreamSize field of the EncryptedPackage field specifies the number of bytes of
|
||||||
|
* unencrypted data as specified in section 2.3.4.4.
|
||||||
|
*/
|
||||||
|
private class AgileCipherOutputStream extends ChunkedCipherOutputStream {
|
||||||
|
public AgileCipherOutputStream(DirectoryNode dir) throws IOException, GeneralSecurityException {
|
||||||
|
super(dir, 4096);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk)
|
||||||
|
throws GeneralSecurityException {
|
||||||
|
return AgileDecryptor.initCipherForBlock(existing, block, lastChunk, builder, getSecretKey(), Cipher.ENCRYPT_MODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void calculateChecksum(File fileOut, int oleStreamSize)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
// integrityHMAC needs to be updated before the encryption document is created
|
||||||
|
updateIntegrityHMAC(fileOut, oleStreamSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)
|
||||||
|
throws IOException, GeneralSecurityException {
|
||||||
|
AgileEncryptor.this.createEncryptionInfoEntry(dir, tmpFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ public class TestAgileEncryptionParameters {
|
||||||
@Parameter(value = 2)
|
@Parameter(value = 2)
|
||||||
public ChainingMode cm;
|
public ChainingMode cm;
|
||||||
|
|
||||||
@Parameters
|
@Parameters(name="{0} {1} {2}")
|
||||||
public static Collection<Object[]> data() {
|
public static Collection<Object[]> data() {
|
||||||
CipherAlgorithm caList[] = { CipherAlgorithm.aes128, CipherAlgorithm.aes192, CipherAlgorithm.aes256, CipherAlgorithm.rc2, CipherAlgorithm.des, CipherAlgorithm.des3 };
|
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 };
|
HashAlgorithm haList[] = { HashAlgorithm.sha1, HashAlgorithm.sha256, HashAlgorithm.sha384, HashAlgorithm.sha512, HashAlgorithm.md5 };
|
||||||
|
@ -86,7 +86,7 @@ public class TestAgileEncryptionParameters {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
POIFSFileSystem fsEnc = new POIFSFileSystem();
|
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();
|
Encryptor enc = infoEnc.getEncryptor();
|
||||||
enc.confirmPassword("foobaa");
|
enc.confirmPassword("foobaa");
|
||||||
OutputStream os = enc.getDataStream(fsEnc);
|
OutputStream os = enc.getDataStream(fsEnc);
|
||||||
|
|
|
@ -29,6 +29,7 @@ import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
import org.apache.poi.POIDataSamples;
|
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.NPOIFSFileSystem;
|
||||||
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
||||||
import org.apache.poi.util.IOUtils;
|
import org.apache.poi.util.IOUtils;
|
||||||
|
@ -60,7 +61,7 @@ public class TestDecryptor {
|
||||||
|
|
||||||
d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
|
d.verifyPassword(Decryptor.DEFAULT_PASSWORD);
|
||||||
|
|
||||||
zipOk(fs, d);
|
zipOk(fs.getRoot(), d);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -75,21 +76,22 @@ public class TestDecryptor {
|
||||||
|
|
||||||
assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
|
assertTrue(d.verifyPassword(Decryptor.DEFAULT_PASSWORD));
|
||||||
|
|
||||||
zipOk(fs, d);
|
zipOk(fs.getRoot(), d);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void zipOk(POIFSFileSystem fs, Decryptor d) throws IOException, GeneralSecurityException {
|
private void zipOk(DirectoryNode root, Decryptor d) throws IOException, GeneralSecurityException {
|
||||||
ZipInputStream zin = new ZipInputStream(d.getDataStream(fs));
|
ZipInputStream zin = new ZipInputStream(d.getDataStream(root));
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
ZipEntry entry = zin.getNextEntry();
|
ZipEntry entry = zin.getNextEntry();
|
||||||
if (entry==null) {
|
if (entry==null) break;
|
||||||
break;
|
// crc32 is checked within zip-stream
|
||||||
}
|
if (entry.isDirectory()) continue;
|
||||||
|
zin.skip(entry.getSize());
|
||||||
while (zin.available()>0) {
|
byte buf[] = new byte[10];
|
||||||
zin.skip(zin.available());
|
int readBytes = zin.read(buf);
|
||||||
}
|
// zin.available() doesn't work for entries
|
||||||
|
assertEquals("size failed for "+entry.getName(), -1, readBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
zin.close();
|
zin.close();
|
||||||
|
|
|
@ -16,9 +16,8 @@
|
||||||
==================================================================== */
|
==================================================================== */
|
||||||
package org.apache.poi.poifs.crypt;
|
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.assertEquals;
|
||||||
import static org.junit.Assert.assertThat;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
@ -49,7 +48,44 @@ import org.junit.Test;
|
||||||
|
|
||||||
public class TestEncryptor {
|
public class TestEncryptor {
|
||||||
@Test
|
@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");
|
int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");
|
||||||
Assume.assumeTrue("Please install JCE Unlimited Strength Jurisdiction Policy files for AES 256", maxKeyLen == 2147483647);
|
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();
|
POIFSFileSystem fs = new POIFSFileSystem();
|
||||||
EncryptionInfo infoActual = new EncryptionInfo(
|
EncryptionInfo infoActual = new EncryptionInfo(
|
||||||
fs, EncryptionMode.agile
|
EncryptionMode.agile
|
||||||
, infoExpected.getVerifier().getCipherAlgorithm()
|
, infoExpected.getVerifier().getCipherAlgorithm()
|
||||||
, infoExpected.getVerifier().getHashAlgorithm()
|
, infoExpected.getVerifier().getHashAlgorithm()
|
||||||
, infoExpected.getHeader().getKeySize()
|
, infoExpected.getHeader().getKeySize()
|
||||||
|
@ -134,14 +170,14 @@ public class TestEncryptor {
|
||||||
|
|
||||||
AgileEncryptionHeader aehExpected = (AgileEncryptionHeader)infoExpected.getHeader();
|
AgileEncryptionHeader aehExpected = (AgileEncryptionHeader)infoExpected.getHeader();
|
||||||
AgileEncryptionHeader aehActual = (AgileEncryptionHeader)infoActual.getHeader();
|
AgileEncryptionHeader aehActual = (AgileEncryptionHeader)infoActual.getHeader();
|
||||||
assertThat(aehExpected.getEncryptedHmacKey(), equalTo(aehActual.getEncryptedHmacKey()));
|
assertArrayEquals(aehExpected.getEncryptedHmacKey(), aehActual.getEncryptedHmacKey());
|
||||||
assertEquals(decPackLenExpected, decPackLenActual);
|
assertEquals(decPackLenExpected, decPackLenActual);
|
||||||
assertThat(payloadExpected, equalTo(payloadActual));
|
assertArrayEquals(payloadExpected, payloadActual);
|
||||||
assertThat(encPackExpected, equalTo(encPackActual));
|
assertArrayEquals(encPackExpected, encPackActual);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStandardEncryption() throws Exception {
|
public void standardEncryption() throws Exception {
|
||||||
File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx");
|
File file = POIDataSamples.getDocumentInstance().getFile("bug53475-password-is-solrcell.docx");
|
||||||
String pass = "solrcell";
|
String pass = "solrcell";
|
||||||
|
|
||||||
|
@ -170,7 +206,7 @@ public class TestEncryptor {
|
||||||
|
|
||||||
POIFSFileSystem fs = new POIFSFileSystem();
|
POIFSFileSystem fs = new POIFSFileSystem();
|
||||||
EncryptionInfo infoActual = new EncryptionInfo(
|
EncryptionInfo infoActual = new EncryptionInfo(
|
||||||
fs, EncryptionMode.standard
|
EncryptionMode.standard
|
||||||
, infoExpected.getVerifier().getCipherAlgorithm()
|
, infoExpected.getVerifier().getCipherAlgorithm()
|
||||||
, infoExpected.getVerifier().getHashAlgorithm()
|
, infoExpected.getVerifier().getHashAlgorithm()
|
||||||
, infoExpected.getHeader().getKeySize()
|
, infoExpected.getHeader().getKeySize()
|
||||||
|
@ -181,15 +217,15 @@ public class TestEncryptor {
|
||||||
Encryptor e = Encryptor.getInstance(infoActual);
|
Encryptor e = Encryptor.getInstance(infoActual);
|
||||||
e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, null);
|
e.confirmPassword(pass, keySpec, keySalt, verifierExpected, verifierSaltExpected, null);
|
||||||
|
|
||||||
assertThat(infoExpected.getVerifier().getEncryptedVerifier(), equalTo(infoActual.getVerifier().getEncryptedVerifier()));
|
assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifier(), infoActual.getVerifier().getEncryptedVerifier());
|
||||||
assertThat(infoExpected.getVerifier().getEncryptedVerifierHash(), equalTo(infoActual.getVerifier().getEncryptedVerifierHash()));
|
assertArrayEquals(infoExpected.getVerifier().getEncryptedVerifierHash(), infoActual.getVerifier().getEncryptedVerifierHash());
|
||||||
|
|
||||||
// now we use a newly generated salt/verifier and check
|
// now we use a newly generated salt/verifier and check
|
||||||
// if the file content is still the same
|
// if the file content is still the same
|
||||||
|
|
||||||
fs = new POIFSFileSystem();
|
fs = new POIFSFileSystem();
|
||||||
infoActual = new EncryptionInfo(
|
infoActual = new EncryptionInfo(
|
||||||
fs, EncryptionMode.standard
|
EncryptionMode.standard
|
||||||
, infoExpected.getVerifier().getCipherAlgorithm()
|
, infoExpected.getVerifier().getCipherAlgorithm()
|
||||||
, infoExpected.getVerifier().getHashAlgorithm()
|
, infoExpected.getVerifier().getHashAlgorithm()
|
||||||
, infoExpected.getHeader().getKeySize()
|
, infoExpected.getHeader().getKeySize()
|
||||||
|
@ -227,12 +263,12 @@ public class TestEncryptor {
|
||||||
nfs.close();
|
nfs.close();
|
||||||
byte payloadActual[] = bos.toByteArray();
|
byte payloadActual[] = bos.toByteArray();
|
||||||
|
|
||||||
assertThat(payloadExpected, equalTo(payloadActual));
|
assertArrayEquals(payloadExpected, payloadActual);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore
|
@Ignore
|
||||||
public void testInPlaceRewrite() throws Exception {
|
public void inPlaceRewrite() throws Exception {
|
||||||
File f = TempFile.createTempFile("protected_agile", ".docx");
|
File f = TempFile.createTempFile("protected_agile", ".docx");
|
||||||
// File f = new File("protected_agile.docx");
|
// File f = new File("protected_agile.docx");
|
||||||
FileOutputStream fos = new FileOutputStream(f);
|
FileOutputStream fos = new FileOutputStream(f);
|
||||||
|
@ -264,10 +300,10 @@ public class TestEncryptor {
|
||||||
|
|
||||||
|
|
||||||
private void listEntry(DocumentNode de, String ext, String path) throws IOException {
|
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)");
|
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);
|
InputStream is = ((DirectoryNode)de.getParent()).createDocumentInputStream(de);
|
||||||
FileOutputStream fos = new FileOutputStream("solr."+name+"."+ext);
|
FileOutputStream fos = new FileOutputStream("solr."+name+"."+ext);
|
||||||
|
|
|
@ -17,117 +17,478 @@
|
||||||
|
|
||||||
package org.apache.poi.hslf;
|
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.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.DocumentEncryptionAtom;
|
||||||
import org.apache.poi.hslf.record.PersistPtrHolder;
|
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.Record;
|
||||||
import org.apache.poi.hslf.record.UserEditAtom;
|
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
|
* This class provides helper functions for encrypted PowerPoint documents.
|
||||||
* 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
|
|
||||||
*/
|
*/
|
||||||
|
@Internal
|
||||||
|
public class EncryptedSlideShow {
|
||||||
|
DocumentEncryptionAtom dea;
|
||||||
|
CryptoAPIEncryptor enc = null;
|
||||||
|
CryptoAPIDecryptor dec = null;
|
||||||
|
Cipher cipher = null;
|
||||||
|
CipherOutputStream cyos = null;
|
||||||
|
|
||||||
public final class EncryptedSlideShow
|
private static final BitField fieldRecInst = new BitField(0xFFF0);
|
||||||
{
|
|
||||||
/**
|
protected EncryptedSlideShow(DocumentEncryptionAtom dea) {
|
||||||
* Check to see if a HSLFSlideShow represents an encrypted
|
this.dea = dea;
|
||||||
* 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,
|
protected EncryptedSlideShow(byte[] docstream, NavigableMap<Integer,Record> recordMap) {
|
||||||
// it's harder.
|
// check for DocumentEncryptionAtom, which would be at the last offset
|
||||||
// We need to see what the last record pointed to by the
|
// need to ignore already set UserEdit and PersistAtoms
|
||||||
// first PersistPrtHolder is - if it's a
|
UserEditAtom userEditAtomWithEncryption = null;
|
||||||
// DocumentEncryptionAtom, then the file's Encrypted
|
for (Map.Entry<Integer, Record> me : recordMap.descendingMap().entrySet()) {
|
||||||
DocumentEncryptionAtom dea = fetchDocumentEncryptionAtom(hss);
|
Record r = me.getValue();
|
||||||
if(dea != null) {
|
if (!(r instanceof UserEditAtom)) continue;
|
||||||
return true;
|
UserEditAtom uea = (UserEditAtom)r;
|
||||||
}
|
if (uea.getEncryptSessionPersistIdRef() != -1) {
|
||||||
return false;
|
userEditAtomWithEncryption = uea;
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
if (userEditAtomWithEncryption == null) {
|
||||||
* Return the DocumentEncryptionAtom for a HSLFSlideShow, or
|
dea = null;
|
||||||
* null if there isn't one.
|
return;
|
||||||
* @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();
|
Record r = recordMap.get(userEditAtomWithEncryption.getPersistPointersOffset());
|
||||||
if(cua.getCurrentEditOffset() != 0) {
|
assert(r instanceof PersistPtrHolder);
|
||||||
// Check it's not past the end of the file
|
PersistPtrHolder ptr = (PersistPtrHolder)r;
|
||||||
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");
|
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()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Grab the details of the UserEditAtom there
|
public DocumentEncryptionAtom getDocumentEncryptionAtom() {
|
||||||
// If the record's messed up, we could AIOOB
|
return dea;
|
||||||
Record r = null;
|
}
|
||||||
try {
|
|
||||||
r = Record.buildRecordAtOffset(
|
protected void setPersistId(int persistId) {
|
||||||
hss.getUnderlyingBytes(),
|
if (enc != null && dec != null) {
|
||||||
(int)cua.getCurrentEditOffset()
|
throw new EncryptedPowerPointFileException("Use instance either for en- or decryption");
|
||||||
);
|
}
|
||||||
} catch (ArrayIndexOutOfBoundsException e) {
|
|
||||||
return null;
|
try {
|
||||||
}
|
if (enc != null) cipher = enc.initCipherForBlock(cipher, persistId);
|
||||||
if(r == null) { return null; }
|
if (dec != null) cipher = dec.initCipherForBlock(cipher, persistId);
|
||||||
if(! (r instanceof UserEditAtom)) { return null; }
|
} catch (GeneralSecurityException e) {
|
||||||
UserEditAtom uea = (UserEditAtom)r;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Now get the PersistPtrHolder
|
|
||||||
Record r2 = Record.buildRecordAtOffset(
|
protected OutputStream encryptRecord(OutputStream plainStream, int persistId, Record record) {
|
||||||
hss.getUnderlyingBytes(),
|
boolean isPlain = (dea == null
|
||||||
uea.getPersistPointersOffset()
|
|| record instanceof UserEditAtom
|
||||||
);
|
|| record instanceof PersistPtrHolder
|
||||||
if(! (r2 instanceof PersistPtrHolder)) { return null; }
|
|| record instanceof DocumentEncryptionAtom
|
||||||
PersistPtrHolder pph = (PersistPtrHolder)r2;
|
);
|
||||||
|
if (isPlain) return plainStream;
|
||||||
|
|
||||||
// Now get the last record
|
encryptInit();
|
||||||
int[] slideIds = pph.getKnownSlideIDs();
|
setPersistId(persistId);
|
||||||
int maxSlideId = -1;
|
|
||||||
for(int i=0; i<slideIds.length; i++) {
|
if (cyos == null) {
|
||||||
if(slideIds[i] > maxSlideId) { maxSlideId = slideIds[i]; }
|
cyos = new CipherOutputStream(plainStream, cipher);
|
||||||
}
|
}
|
||||||
if(maxSlideId == -1) { return null; }
|
return cyos;
|
||||||
|
}
|
||||||
|
|
||||||
int offset = (
|
protected void decryptRecord(byte[] docstream, int persistId, int offset) {
|
||||||
(Integer)pph.getSlideLocationsLookup().get(
|
if (dea == null) return;
|
||||||
Integer.valueOf(maxSlideId)
|
|
||||||
) ).intValue();
|
|
||||||
Record r3 = Record.buildRecordAtOffset(
|
|
||||||
hss.getUnderlyingBytes(),
|
|
||||||
offset
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we have a DocumentEncryptionAtom, it'll be this one
|
decryptInit();
|
||||||
if(r3 instanceof DocumentEncryptionAtom) {
|
setPersistId(persistId);
|
||||||
return (DocumentEncryptionAtom)r3;
|
|
||||||
}
|
try {
|
||||||
}
|
// decrypt header and read length to be decrypted
|
||||||
|
cipher.update(docstream, offset, 8, docstream, offset);
|
||||||
|
// decrypt the rest of the record
|
||||||
|
int rlen = (int)LittleEndian.getUInt(docstream, offset+4);
|
||||||
|
cipher.update(docstream, offset+8, rlen, docstream, offset+8);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new CorruptPowerPointFileException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void decryptPicture(byte[] pictstream, int offset) {
|
||||||
|
if (dea == null) return;
|
||||||
|
|
||||||
|
decryptInit();
|
||||||
|
setPersistId(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// decrypt header and read length to be decrypted
|
||||||
|
cipher.doFinal(pictstream, offset, 8, pictstream, offset);
|
||||||
|
int recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
|
||||||
|
int recType = LittleEndian.getUShort(pictstream, offset+2);
|
||||||
|
int rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
|
||||||
|
offset += 8;
|
||||||
|
int endOffset = offset + rlen;
|
||||||
|
|
||||||
|
if (recType == 0xF007) {
|
||||||
|
// TOOD: get a real example file ... to actual test the FBSE entry
|
||||||
|
// not sure where the foDelay block is
|
||||||
|
|
||||||
|
// File BLIP Store Entry (FBSE)
|
||||||
|
cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btWin32
|
||||||
|
offset++;
|
||||||
|
cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btMacOS
|
||||||
|
offset++;
|
||||||
|
cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid
|
||||||
|
offset += 16;
|
||||||
|
cipher.doFinal(pictstream, offset, 2, pictstream, offset); // tag
|
||||||
|
offset += 2;
|
||||||
|
cipher.doFinal(pictstream, offset, 4, pictstream, offset); // size
|
||||||
|
offset += 4;
|
||||||
|
cipher.doFinal(pictstream, offset, 4, pictstream, offset); // cRef
|
||||||
|
offset += 4;
|
||||||
|
cipher.doFinal(pictstream, offset, 4, pictstream, offset); // foDelay
|
||||||
|
offset += 4;
|
||||||
|
cipher.doFinal(pictstream, offset+0, 1, pictstream, offset+0); // unused1
|
||||||
|
cipher.doFinal(pictstream, offset+1, 1, pictstream, offset+1); // cbName
|
||||||
|
cipher.doFinal(pictstream, offset+2, 1, pictstream, offset+2); // unused2
|
||||||
|
cipher.doFinal(pictstream, offset+3, 1, pictstream, offset+3); // unused3
|
||||||
|
int cbName = LittleEndian.getUShort(pictstream, offset+1);
|
||||||
|
offset += 4;
|
||||||
|
if (cbName > 0) {
|
||||||
|
cipher.doFinal(pictstream, offset, cbName, pictstream, offset); // nameData
|
||||||
|
offset += cbName;
|
||||||
|
}
|
||||||
|
if (offset == endOffset) {
|
||||||
|
return; // no embedded blip
|
||||||
|
}
|
||||||
|
// fall through, read embedded blip now
|
||||||
|
|
||||||
|
// update header data
|
||||||
|
cipher.doFinal(pictstream, offset, 8, pictstream, offset);
|
||||||
|
recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
|
||||||
|
recType = LittleEndian.getUShort(pictstream, offset+2);
|
||||||
|
rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
|
||||||
|
offset += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rgbUidCnt = (recInst == 0x217 || recInst == 0x3D5 || recInst == 0x46B || recInst == 0x543 ||
|
||||||
|
recInst == 0x6E1 || recInst == 0x6E3 || recInst == 0x6E5 || recInst == 0x7A9) ? 2 : 1;
|
||||||
|
|
||||||
|
for (int i=0; i<rgbUidCnt; i++) {
|
||||||
|
cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid 1/2
|
||||||
|
offset += 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recType == 0xF01A || recType == 0XF01B || recType == 0XF01C) {
|
||||||
|
cipher.doFinal(pictstream, offset, 34, pictstream, offset); // metafileHeader
|
||||||
|
offset += 34;
|
||||||
|
} else {
|
||||||
|
cipher.doFinal(pictstream, offset, 1, pictstream, offset); // tag
|
||||||
|
offset += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int blipLen = endOffset - offset;
|
||||||
|
cipher.doFinal(pictstream, offset, blipLen, pictstream, offset);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new CorruptPowerPointFileException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void encryptPicture(byte[] pictstream, int offset) {
|
||||||
|
if (dea == null) return;
|
||||||
|
|
||||||
|
encryptInit();
|
||||||
|
setPersistId(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
|
||||||
|
int recType = LittleEndian.getUShort(pictstream, offset+2);
|
||||||
|
int rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
|
||||||
|
cipher.doFinal(pictstream, offset, 8, pictstream, offset);
|
||||||
|
offset += 8;
|
||||||
|
int endOffset = offset + rlen;
|
||||||
|
|
||||||
|
if (recType == 0xF007) {
|
||||||
|
// TOOD: get a real example file ... to actual test the FBSE entry
|
||||||
|
// not sure where the foDelay block is
|
||||||
|
|
||||||
|
// File BLIP Store Entry (FBSE)
|
||||||
|
cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btWin32
|
||||||
|
offset++;
|
||||||
|
cipher.doFinal(pictstream, offset, 1, pictstream, offset); // btMacOS
|
||||||
|
offset++;
|
||||||
|
cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid
|
||||||
|
offset += 16;
|
||||||
|
cipher.doFinal(pictstream, offset, 2, pictstream, offset); // tag
|
||||||
|
offset += 2;
|
||||||
|
cipher.doFinal(pictstream, offset, 4, pictstream, offset); // size
|
||||||
|
offset += 4;
|
||||||
|
cipher.doFinal(pictstream, offset, 4, pictstream, offset); // cRef
|
||||||
|
offset += 4;
|
||||||
|
cipher.doFinal(pictstream, offset, 4, pictstream, offset); // foDelay
|
||||||
|
offset += 4;
|
||||||
|
int cbName = LittleEndian.getUShort(pictstream, offset+1);
|
||||||
|
cipher.doFinal(pictstream, offset+0, 1, pictstream, offset+0); // unused1
|
||||||
|
cipher.doFinal(pictstream, offset+1, 1, pictstream, offset+1); // cbName
|
||||||
|
cipher.doFinal(pictstream, offset+2, 1, pictstream, offset+2); // unused2
|
||||||
|
cipher.doFinal(pictstream, offset+3, 1, pictstream, offset+3); // unused3
|
||||||
|
offset += 4;
|
||||||
|
if (cbName > 0) {
|
||||||
|
cipher.doFinal(pictstream, offset, cbName, pictstream, offset); // nameData
|
||||||
|
offset += cbName;
|
||||||
|
}
|
||||||
|
if (offset == endOffset) {
|
||||||
|
return; // no embedded blip
|
||||||
|
}
|
||||||
|
// fall through, read embedded blip now
|
||||||
|
|
||||||
|
// update header data
|
||||||
|
recInst = fieldRecInst.getValue(LittleEndian.getUShort(pictstream, offset));
|
||||||
|
recType = LittleEndian.getUShort(pictstream, offset+2);
|
||||||
|
rlen = (int)LittleEndian.getUInt(pictstream, offset+4);
|
||||||
|
cipher.doFinal(pictstream, offset, 8, pictstream, offset);
|
||||||
|
offset += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rgbUidCnt = (recInst == 0x217 || recInst == 0x3D5 || recInst == 0x46B || recInst == 0x543 ||
|
||||||
|
recInst == 0x6E1 || recInst == 0x6E3 || recInst == 0x6E5 || recInst == 0x7A9) ? 2 : 1;
|
||||||
|
|
||||||
|
for (int i=0; i<rgbUidCnt; i++) {
|
||||||
|
cipher.doFinal(pictstream, offset, 16, pictstream, offset); // rgbUid 1/2
|
||||||
|
offset += 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recType == 0xF01A || recType == 0XF01B || recType == 0XF01C) {
|
||||||
|
cipher.doFinal(pictstream, offset, 34, pictstream, offset); // metafileHeader
|
||||||
|
offset += 34;
|
||||||
|
} else {
|
||||||
|
cipher.doFinal(pictstream, offset, 1, pictstream, offset); // tag
|
||||||
|
offset += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int blipLen = endOffset - offset;
|
||||||
|
cipher.doFinal(pictstream, offset, blipLen, pictstream, offset);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new CorruptPowerPointFileException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Record[] updateEncryptionRecord(Record records[]) {
|
||||||
|
String password = Biff8EncryptionKey.getCurrentUserPassword();
|
||||||
|
if (password == null) {
|
||||||
|
if (dea == null) {
|
||||||
|
// no password given, no encryption record exits -> done
|
||||||
|
return records;
|
||||||
|
} else {
|
||||||
|
// need to remove password data
|
||||||
|
dea = null;
|
||||||
|
return removeEncryptionRecord(records);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// create password record
|
||||||
|
if (dea == null) {
|
||||||
|
dea = new DocumentEncryptionAtom();
|
||||||
|
}
|
||||||
|
EncryptionInfo ei = dea.getEncryptionInfo();
|
||||||
|
byte salt[] = ei.getVerifier().getSalt();
|
||||||
|
Encryptor enc = ei.getEncryptor();
|
||||||
|
if (salt == null) {
|
||||||
|
enc.confirmPassword(password);
|
||||||
|
} else {
|
||||||
|
byte verifier[] = ei.getDecryptor().getVerifier();
|
||||||
|
enc.confirmPassword(password, null, null, verifier, salt, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// move EncryptionRecord to last slide position
|
||||||
|
records = normalizeRecords(records);
|
||||||
|
return addEncryptionRecord(records, dea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove duplicated UserEditAtoms and merge PersistPtrHolder.
|
||||||
|
* Before this method is called, make sure that the offsets are correct,
|
||||||
|
* i.e. call {@link HSLFSlideShow#updateAndWriteDependantRecords(OutputStream, Map)}
|
||||||
|
*/
|
||||||
|
protected static Record[] normalizeRecords(Record records[]) {
|
||||||
|
// http://msdn.microsoft.com/en-us/library/office/gg615594(v=office.14).aspx
|
||||||
|
// repeated slideIds can be overwritten, i.e. ignored
|
||||||
|
|
||||||
|
UserEditAtom uea = null;
|
||||||
|
PersistPtrHolder pph = null;
|
||||||
|
TreeMap<Integer,Integer> slideLocations = new TreeMap<Integer,Integer>();
|
||||||
|
TreeMap<Integer,Record> recordMap = new TreeMap<Integer,Record>();
|
||||||
|
List<Integer> obsoleteOffsets = new ArrayList<Integer>();
|
||||||
|
int duplicatedCount = 0;
|
||||||
|
for (Record r : records) {
|
||||||
|
assert(r instanceof PositionDependentRecord);
|
||||||
|
PositionDependentRecord pdr = (PositionDependentRecord)r;
|
||||||
|
if (pdr instanceof UserEditAtom) {
|
||||||
|
uea = (UserEditAtom)pdr;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdr instanceof PersistPtrHolder) {
|
||||||
|
if (pph != null) {
|
||||||
|
duplicatedCount++;
|
||||||
|
}
|
||||||
|
pph = (PersistPtrHolder)pdr;
|
||||||
|
for (Map.Entry<Integer,Integer> me : pph.getSlideLocationsLookup().entrySet()) {
|
||||||
|
Integer oldOffset = slideLocations.put(me.getKey(), me.getValue());
|
||||||
|
if (oldOffset != null) obsoleteOffsets.add(oldOffset);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordMap.put(pdr.getLastOnDiskOffset(), r);
|
||||||
|
}
|
||||||
|
recordMap.put(pph.getLastOnDiskOffset(), pph);
|
||||||
|
recordMap.put(uea.getLastOnDiskOffset(), uea);
|
||||||
|
|
||||||
|
assert(uea != null && pph != null && uea.getPersistPointersOffset() == pph.getLastOnDiskOffset());
|
||||||
|
|
||||||
|
if (duplicatedCount == 0 && obsoleteOffsets.isEmpty()) {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
uea.setLastUserEditAtomOffset(0);
|
||||||
|
pph.clear();
|
||||||
|
for (Map.Entry<Integer,Integer> me : slideLocations.entrySet()) {
|
||||||
|
pph.addSlideLookup(me.getKey(), me.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Integer oldOffset : obsoleteOffsets) {
|
||||||
|
recordMap.remove(oldOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordMap.values().toArray(new Record[recordMap.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected static Record[] removeEncryptionRecord(Record records[]) {
|
||||||
|
int deaSlideId = -1;
|
||||||
|
int deaOffset = -1;
|
||||||
|
PersistPtrHolder ptr = null;
|
||||||
|
UserEditAtom uea = null;
|
||||||
|
List<Record> recordList = new ArrayList<Record>();
|
||||||
|
for (Record r : records) {
|
||||||
|
if (r instanceof DocumentEncryptionAtom) {
|
||||||
|
deaOffset = ((DocumentEncryptionAtom)r).getLastOnDiskOffset();
|
||||||
|
continue;
|
||||||
|
} else if (r instanceof UserEditAtom) {
|
||||||
|
uea = (UserEditAtom)r;
|
||||||
|
deaSlideId = uea.getEncryptSessionPersistIdRef();
|
||||||
|
uea.setEncryptSessionPersistIdRef(-1);
|
||||||
|
} else if (r instanceof PersistPtrHolder) {
|
||||||
|
ptr = (PersistPtrHolder)r;
|
||||||
|
}
|
||||||
|
recordList.add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(ptr != null);
|
||||||
|
if (deaSlideId == -1 && deaOffset == -1) return records;
|
||||||
|
|
||||||
|
TreeMap<Integer,Integer> tm = new TreeMap<Integer,Integer>(ptr.getSlideLocationsLookup());
|
||||||
|
ptr.clear();
|
||||||
|
int maxSlideId = -1;
|
||||||
|
for (Map.Entry<Integer,Integer> me : tm.entrySet()) {
|
||||||
|
if (me.getKey() == deaSlideId || me.getValue() == deaOffset) continue;
|
||||||
|
ptr.addSlideLookup(me.getKey(), me.getValue());
|
||||||
|
maxSlideId = Math.max(me.getKey(), maxSlideId);
|
||||||
|
}
|
||||||
|
|
||||||
|
uea.setMaxPersistWritten(maxSlideId);
|
||||||
|
|
||||||
|
records = recordList.toArray(new Record[recordList.size()]);
|
||||||
|
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected static Record[] addEncryptionRecord(Record records[], DocumentEncryptionAtom dea) {
|
||||||
|
assert(dea != null);
|
||||||
|
int ueaIdx = -1, ptrIdx = -1, deaIdx = -1, idx = -1;
|
||||||
|
for (Record r : records) {
|
||||||
|
idx++;
|
||||||
|
if (r instanceof UserEditAtom) ueaIdx = idx;
|
||||||
|
else if (r instanceof PersistPtrHolder) ptrIdx = idx;
|
||||||
|
else if (r instanceof DocumentEncryptionAtom) deaIdx = idx;
|
||||||
|
}
|
||||||
|
assert(ueaIdx != -1 && ptrIdx != -1 && ptrIdx < ueaIdx);
|
||||||
|
if (deaIdx != -1) {
|
||||||
|
DocumentEncryptionAtom deaOld = (DocumentEncryptionAtom)records[deaIdx];
|
||||||
|
dea.setLastOnDiskOffset(deaOld.getLastOnDiskOffset());
|
||||||
|
records[deaIdx] = dea;
|
||||||
|
return records;
|
||||||
|
} else {
|
||||||
|
PersistPtrHolder ptr = (PersistPtrHolder)records[ptrIdx];
|
||||||
|
UserEditAtom uea = ((UserEditAtom)records[ueaIdx]);
|
||||||
|
dea.setLastOnDiskOffset(ptr.getLastOnDiskOffset()-1);
|
||||||
|
int nextSlideId = uea.getMaxPersistWritten()+1;
|
||||||
|
ptr.addSlideLookup(nextSlideId, ptr.getLastOnDiskOffset()-1);
|
||||||
|
uea.setEncryptSessionPersistIdRef(nextSlideId);
|
||||||
|
uea.setMaxPersistWritten(nextSlideId);
|
||||||
|
|
||||||
|
Record newRecords[] = new Record[records.length+1];
|
||||||
|
if (ptrIdx > 0) System.arraycopy(records, 0, newRecords, 0, ptrIdx);
|
||||||
|
if (ptrIdx < records.length-1) System.arraycopy(records, ptrIdx, newRecords, ptrIdx+1, records.length-ptrIdx);
|
||||||
|
newRecords[ptrIdx] = dea;
|
||||||
|
return newRecords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Hashtable;
|
import java.util.Hashtable;
|
||||||
|
@ -32,10 +33,11 @@ import java.util.NavigableMap;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
|
||||||
import org.apache.poi.POIDocument;
|
import org.apache.poi.POIDocument;
|
||||||
|
import org.apache.poi.hpsf.PropertySet;
|
||||||
import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
|
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.exceptions.HSLFException;
|
||||||
import org.apache.poi.hslf.record.CurrentUserAtom;
|
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.ExOleObjStg;
|
||||||
import org.apache.poi.hslf.record.PersistPtrHolder;
|
import org.apache.poi.hslf.record.PersistPtrHolder;
|
||||||
import org.apache.poi.hslf.record.PersistRecord;
|
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.record.UserEditAtom;
|
||||||
import org.apache.poi.hslf.usermodel.ObjectData;
|
import org.apache.poi.hslf.usermodel.ObjectData;
|
||||||
import org.apache.poi.hslf.usermodel.PictureData;
|
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.DirectoryNode;
|
||||||
import org.apache.poi.poifs.filesystem.DocumentEntry;
|
import org.apache.poi.poifs.filesystem.DocumentEntry;
|
||||||
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
||||||
|
@ -182,13 +185,6 @@ public final class HSLFSlideShow extends POIDocument {
|
||||||
// PowerPoint stream
|
// PowerPoint stream
|
||||||
readPowerPointStream();
|
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
|
// Now, build records based on the PowerPoint stream
|
||||||
buildRecords();
|
buildRecords();
|
||||||
|
|
||||||
|
@ -278,6 +274,7 @@ public final class HSLFSlideShow extends POIDocument {
|
||||||
NavigableMap<Integer,Record> records = new TreeMap<Integer,Record>(); // offset -> record
|
NavigableMap<Integer,Record> records = new TreeMap<Integer,Record>(); // offset -> record
|
||||||
Map<Integer,Integer> persistIds = new HashMap<Integer,Integer>(); // offset -> persistId
|
Map<Integer,Integer> persistIds = new HashMap<Integer,Integer>(); // offset -> persistId
|
||||||
initRecordOffsets(docstream, usrOffset, records, persistIds);
|
initRecordOffsets(docstream, usrOffset, records, persistIds);
|
||||||
|
EncryptedSlideShow decryptData = new EncryptedSlideShow(docstream, records);
|
||||||
|
|
||||||
for (Map.Entry<Integer,Record> entry : records.entrySet()) {
|
for (Map.Entry<Integer,Record> entry : records.entrySet()) {
|
||||||
Integer offset = entry.getKey();
|
Integer offset = entry.getKey();
|
||||||
|
@ -286,6 +283,7 @@ public final class HSLFSlideShow extends POIDocument {
|
||||||
if (record == null) {
|
if (record == null) {
|
||||||
// all plain records have been already added,
|
// all plain records have been already added,
|
||||||
// only new records need to be decrypted (tbd #35897)
|
// only new records need to be decrypted (tbd #35897)
|
||||||
|
decryptData.decryptRecord(docstream, persistId, offset);
|
||||||
record = Record.buildRecordAtOffset(docstream, offset);
|
record = Record.buildRecordAtOffset(docstream, offset);
|
||||||
entry.setValue(record);
|
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
|
* Find the "Current User" stream, and load it
|
||||||
*/
|
*/
|
||||||
|
@ -353,6 +361,7 @@ public final class HSLFSlideShow extends POIDocument {
|
||||||
private void readOtherStreams() {
|
private void readOtherStreams() {
|
||||||
// Currently, there aren't any
|
// Currently, there aren't any
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find and read in pictures contained in this presentation.
|
* Find and read in pictures contained in this presentation.
|
||||||
* This is lazily called as and when we want to touch pictures.
|
* 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 the presentation doesn't contain pictures - will use a null set instead
|
||||||
if (!directory.hasEntry("Pictures")) return;
|
if (!directory.hasEntry("Pictures")) return;
|
||||||
|
|
||||||
|
EncryptedSlideShow decryptData = new EncryptedSlideShow(getDocumentEncryptionAtom());
|
||||||
|
|
||||||
DocumentEntry entry = (DocumentEntry)directory.getEntry("Pictures");
|
DocumentEntry entry = (DocumentEntry)directory.getEntry("Pictures");
|
||||||
byte[] pictstream = new byte[entry.getSize()];
|
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
|
// An empty picture record (length 0) will take up 8 bytes
|
||||||
while (pos <= (pictstream.length-8)) {
|
while (pos <= (pictstream.length-8)) {
|
||||||
int offset = pos;
|
int offset = pos;
|
||||||
|
|
||||||
|
decryptData.decryptPicture(pictstream, offset);
|
||||||
|
|
||||||
// Image signature
|
// Image signature
|
||||||
int signature = LittleEndian.getUShort(pictstream, pos);
|
int signature = LittleEndian.getUShort(pictstream, pos);
|
||||||
|
@ -422,7 +435,21 @@ public final class HSLFSlideShow extends POIDocument {
|
||||||
pos += imgsize;
|
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
|
* This is a helper functions, which is needed for adding new position dependent records
|
||||||
* or finally write the slideshow to a file.
|
* 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
|
// records are going to end up, in the new scheme
|
||||||
// (Annoyingly, some powerpoint files have PersistPtrHolders
|
// (Annoyingly, some powerpoint files have PersistPtrHolders
|
||||||
// that reference slides after the PersistPtrHolder)
|
// that reference slides after the PersistPtrHolder)
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
UserEditAtom usr = null;
|
||||||
|
PersistPtrHolder ptr = null;
|
||||||
|
CountingOS cos = new CountingOS();
|
||||||
for (Record record : _records) {
|
for (Record record : _records) {
|
||||||
if(record instanceof PositionDependentRecord) {
|
// all top level records are position dependent
|
||||||
PositionDependentRecord pdr = (PositionDependentRecord)record;
|
assert(record instanceof PositionDependentRecord);
|
||||||
int oldPos = pdr.getLastOnDiskOffset();
|
PositionDependentRecord pdr = (PositionDependentRecord)record;
|
||||||
int newPos = baos.size();
|
int oldPos = pdr.getLastOnDiskOffset();
|
||||||
pdr.setLastOnDiskOffset(newPos);
|
int newPos = cos.size();
|
||||||
if (oldPos != UNSET_OFFSET) {
|
pdr.setLastOnDiskOffset(newPos);
|
||||||
// new records don't need a mapping, as they aren't in a relation yet
|
if (oldPos != UNSET_OFFSET) {
|
||||||
oldToNewPositions.put(Integer.valueOf(oldPos),Integer.valueOf(newPos));
|
// 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
|
// 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
|
assert(usr != null && ptr != null);
|
||||||
// happen at the top level.
|
|
||||||
// In future, we'll need the handle them everywhere, but that's
|
Map<Integer,Integer> persistIds = new HashMap<Integer,Integer>();
|
||||||
// a bit trickier
|
for (Map.Entry<Integer,Integer> entry : ptr.getSlideLocationsLookup().entrySet()) {
|
||||||
UserEditAtom usr = null;
|
persistIds.put(oldToNewPositions.get(entry.getValue()), entry.getKey());
|
||||||
for (Record record : _records) {
|
}
|
||||||
if (record instanceof PositionDependentRecord) {
|
|
||||||
// We've already figured out their new location, and
|
EncryptedSlideShow encData = new EncryptedSlideShow(getDocumentEncryptionAtom());
|
||||||
// told them that
|
|
||||||
// Tell them of the positions of the other records though
|
for (Record record : _records) {
|
||||||
PositionDependentRecord pdr = (PositionDependentRecord)record;
|
assert(record instanceof PositionDependentRecord);
|
||||||
pdr.updateOtherRecordReferences(oldToNewPositions);
|
// We've already figured out their new location, and
|
||||||
|
// told them that
|
||||||
// Grab interesting records as they come past
|
// Tell them of the positions of the other records though
|
||||||
// this will only save the very last record of each type
|
PositionDependentRecord pdr = (PositionDependentRecord)record;
|
||||||
RecordTypes.Type saveme = null;
|
Integer persistId = persistIds.get(pdr.getLastOnDiskOffset());
|
||||||
int recordType = (int)record.getRecordType();
|
if (persistId == null) persistId = 0;
|
||||||
if (recordType == RecordTypes.PersistPtrIncrementalBlock.typeID) {
|
|
||||||
saveme = RecordTypes.PersistPtrIncrementalBlock;
|
// For now, we're only handling PositionDependentRecord's that
|
||||||
} else if (recordType == RecordTypes.UserEditAtom.typeID) {
|
// happen at the top level.
|
||||||
saveme = RecordTypes.UserEditAtom;
|
// In future, we'll need the handle them everywhere, but that's
|
||||||
usr = (UserEditAtom)pdr;
|
// a bit trickier
|
||||||
}
|
pdr.updateOtherRecordReferences(oldToNewPositions);
|
||||||
if (interestingRecords != null && saveme != null) {
|
|
||||||
interestingRecords.put(saveme,pdr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whatever happens, write out that record tree
|
// Whatever happens, write out that record tree
|
||||||
if (os != null) {
|
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());
|
currentUser.setCurrentEditOffset(usr.getLastOnDiskOffset());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes out the slideshow file the is represented by an instance
|
* Writes out the slideshow file the is represented by an instance
|
||||||
* of this class.
|
* of this class.
|
||||||
|
@ -529,6 +568,16 @@ public final class HSLFSlideShow extends POIDocument {
|
||||||
* the passed in OutputStream
|
* the passed in OutputStream
|
||||||
*/
|
*/
|
||||||
public void write(OutputStream out, boolean preserveNodes) throws IOException {
|
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
|
// Get a new Filesystem to write into
|
||||||
POIFSFileSystem outFS = new POIFSFileSystem();
|
POIFSFileSystem outFS = new POIFSFileSystem();
|
||||||
|
|
||||||
|
@ -537,8 +586,8 @@ public final class HSLFSlideShow extends POIDocument {
|
||||||
|
|
||||||
// Write out the Property Streams
|
// Write out the Property Streams
|
||||||
writeProperties(outFS, writtenEntries);
|
writeProperties(outFS, writtenEntries);
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
BufAccessBAOS baos = new BufAccessBAOS();
|
||||||
|
|
||||||
// For position dependent records, hold where they were and now are
|
// For position dependent records, hold where they were and now are
|
||||||
// As we go along, update, and hand over, to any Position Dependent
|
// 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);
|
updateAndWriteDependantRecords(baos, null);
|
||||||
|
|
||||||
// Update our cached copy of the bytes that make up the PPT stream
|
// 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
|
// Write the PPT stream into the POIFS layer
|
||||||
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
ByteArrayInputStream bais = new ByteArrayInputStream(_docstream);
|
||||||
outFS.createDocument(bais,"PowerPoint Document");
|
outFS.createDocument(bais,"PowerPoint Document");
|
||||||
writtenEntries.add("PowerPoint Document");
|
writtenEntries.add("PowerPoint Document");
|
||||||
|
|
||||||
|
currentUser.setEncrypted(encryptedSS.getDocumentEncryptionAtom() != null);
|
||||||
currentUser.writeToFS(outFS);
|
currentUser.writeToFS(outFS);
|
||||||
writtenEntries.add("Current User");
|
writtenEntries.add("Current User");
|
||||||
|
|
||||||
|
|
||||||
// Write any pictures, into another stream
|
|
||||||
if(_pictures == null) {
|
|
||||||
readPictures();
|
|
||||||
}
|
|
||||||
if (_pictures.size() > 0) {
|
if (_pictures.size() > 0) {
|
||||||
ByteArrayOutputStream pict = new ByteArrayOutputStream();
|
BufAccessBAOS pict = new BufAccessBAOS();
|
||||||
for (PictureData p : _pictures) {
|
for (PictureData p : _pictures) {
|
||||||
|
int offset = pict.size();
|
||||||
p.write(pict);
|
p.write(pict);
|
||||||
|
encryptedSS.encryptPicture(pict.getBuf(), offset);
|
||||||
}
|
}
|
||||||
outFS.createDocument(
|
outFS.createDocument(
|
||||||
new ByteArrayInputStream(pict.toByteArray()), "Pictures"
|
new ByteArrayInputStream(pict.getBuf(), 0, pict.size()), "Pictures"
|
||||||
);
|
);
|
||||||
writtenEntries.add("Pictures");
|
writtenEntries.add("Pictures");
|
||||||
}
|
}
|
||||||
|
@ -580,8 +630,44 @@ public final class HSLFSlideShow extends POIDocument {
|
||||||
outFS.writeFilesystem(out);
|
outFS.writeFilesystem(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a given named property entry, either return it or null if
|
||||||
|
* if it wasn't found
|
||||||
|
*
|
||||||
|
* @param setName The property to read
|
||||||
|
* @return The value of the given property or null if it wasn't found.
|
||||||
|
*/
|
||||||
|
protected PropertySet getPropertySet(String setName) {
|
||||||
|
DocumentEncryptionAtom dea = getDocumentEncryptionAtom();
|
||||||
|
return (dea == null)
|
||||||
|
? super.getPropertySet(setName)
|
||||||
|
: super.getPropertySet(setName, dea.getEncryptionInfo());
|
||||||
|
}
|
||||||
|
|
||||||
/* ******************* adding methods follow ********************* */
|
/**
|
||||||
|
* Writes out the standard Documment Information Properties (HPSF)
|
||||||
|
* @param outFS the POIFSFileSystem to write the properties into
|
||||||
|
* @param writtenEntries a list of POIFS entries to add the property names too
|
||||||
|
*
|
||||||
|
* @throws IOException if an error when writing to the
|
||||||
|
* {@link POIFSFileSystem} occurs
|
||||||
|
*/
|
||||||
|
protected void writeProperties(POIFSFileSystem outFS, List<String> writtenEntries) throws IOException {
|
||||||
|
super.writeProperties(outFS, writtenEntries);
|
||||||
|
DocumentEncryptionAtom dea = getDocumentEncryptionAtom();
|
||||||
|
if (dea != null) {
|
||||||
|
CryptoAPIEncryptor enc = (CryptoAPIEncryptor)dea.getEncryptionInfo().getEncryptor();
|
||||||
|
try {
|
||||||
|
enc.getDataStream(outFS.getRoot()); // ignore OutputStream
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ******************* adding methods follow ********************* */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new root level record, at the end, but before the last
|
* 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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,23 @@
|
||||||
|
|
||||||
package org.apache.poi.hslf.dev;
|
package org.apache.poi.hslf.dev;
|
||||||
|
|
||||||
import org.apache.poi.hslf.*;
|
import java.io.ByteArrayOutputStream;
|
||||||
import org.apache.poi.hslf.record.*;
|
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.hslf.usermodel.SlideShow;
|
||||||
|
|
||||||
import org.apache.poi.util.LittleEndian;
|
import org.apache.poi.util.LittleEndian;
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.util.Hashtable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all the different things that have Slide IDs (of sorts)
|
* Gets all the different things that have Slide IDs (of sorts)
|
||||||
* in them, and displays them, so you can try to guess what they
|
* 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
|
// Check the sheet offsets
|
||||||
int[] sheetIDs = pph.getKnownSlideIDs();
|
int[] sheetIDs = pph.getKnownSlideIDs();
|
||||||
Hashtable sheetOffsets = pph.getSlideLocationsLookup();
|
Map<Integer,Integer> sheetOffsets = pph.getSlideLocationsLookup();
|
||||||
for(int j=0; j<sheetIDs.length; j++) {
|
for(int j=0; j<sheetIDs.length; j++) {
|
||||||
Integer id = Integer.valueOf(sheetIDs[j]);
|
Integer id = sheetIDs[j];
|
||||||
Integer offset = (Integer)sheetOffsets.get(id);
|
Integer offset = sheetOffsets.get(id);
|
||||||
|
|
||||||
System.out.println(" Knows about sheet " + id);
|
System.out.println(" Knows about sheet " + id);
|
||||||
System.out.println(" That sheet lives at " + offset);
|
System.out.println(" That sheet lives at " + offset);
|
||||||
|
|
|
@ -18,10 +18,14 @@
|
||||||
package org.apache.poi.hslf.dev;
|
package org.apache.poi.hslf.dev;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.Hashtable;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.poi.hslf.HSLFSlideShow;
|
import org.apache.poi.hslf.HSLFSlideShow;
|
||||||
import org.apache.poi.hslf.record.*;
|
import org.apache.poi.hslf.record.CurrentUserAtom;
|
||||||
|
import org.apache.poi.hslf.record.PersistPtrHolder;
|
||||||
|
import org.apache.poi.hslf.record.PositionDependentRecord;
|
||||||
|
import org.apache.poi.hslf.record.Record;
|
||||||
|
import org.apache.poi.hslf.record.UserEditAtom;
|
||||||
import org.apache.poi.util.LittleEndian;
|
import org.apache.poi.util.LittleEndian;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,10 +65,10 @@ public final class UserEditAndPersistListing {
|
||||||
|
|
||||||
// Check the sheet offsets
|
// Check the sheet offsets
|
||||||
int[] sheetIDs = pph.getKnownSlideIDs();
|
int[] sheetIDs = pph.getKnownSlideIDs();
|
||||||
Hashtable sheetOffsets = pph.getSlideLocationsLookup();
|
Map<Integer,Integer> sheetOffsets = pph.getSlideLocationsLookup();
|
||||||
for(int j=0; j<sheetIDs.length; j++) {
|
for(int j=0; j<sheetIDs.length; j++) {
|
||||||
Integer id = Integer.valueOf(sheetIDs[j]);
|
Integer id = sheetIDs[j];
|
||||||
Integer offset = (Integer)sheetOffsets.get(id);
|
Integer offset = sheetOffsets.get(id);
|
||||||
|
|
||||||
System.out.println(" Knows about sheet " + id);
|
System.out.println(" Knows about sheet " + id);
|
||||||
System.out.println(" That sheet lives at " + offset);
|
System.out.println(" That sheet lives at " + offset);
|
||||||
|
|
|
@ -29,4 +29,12 @@ public final class CorruptPowerPointFileException extends IllegalStateException
|
||||||
public CorruptPowerPointFileException(String s) {
|
public CorruptPowerPointFileException(String s) {
|
||||||
super(s);
|
super(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CorruptPowerPointFileException(String s, Throwable t) {
|
||||||
|
super(s,t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CorruptPowerPointFileException(Throwable t) {
|
||||||
|
super(t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,4 +28,12 @@ public final class EncryptedPowerPointFileException extends EncryptedDocumentExc
|
||||||
public EncryptedPowerPointFileException(String s) {
|
public EncryptedPowerPointFileException(String s) {
|
||||||
super(s);
|
super(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EncryptedPowerPointFileException(String s, Throwable t) {
|
||||||
|
super(s, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EncryptedPowerPointFileException(Throwable t) {
|
||||||
|
super(t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,15 +20,21 @@
|
||||||
|
|
||||||
package org.apache.poi.hslf.record;
|
package org.apache.poi.hslf.record;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.ByteArrayInputStream;
|
||||||
import org.apache.poi.poifs.filesystem.*;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
|
||||||
|
import org.apache.poi.hslf.exceptions.OldPowerPointFormatException;
|
||||||
|
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
||||||
|
import org.apache.poi.poifs.filesystem.DocumentEntry;
|
||||||
|
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
||||||
import org.apache.poi.util.LittleEndian;
|
import org.apache.poi.util.LittleEndian;
|
||||||
import org.apache.poi.util.POILogFactory;
|
import org.apache.poi.util.POILogFactory;
|
||||||
import org.apache.poi.util.POILogger;
|
import org.apache.poi.util.POILogger;
|
||||||
import org.apache.poi.util.StringUtil;
|
import org.apache.poi.util.StringUtil;
|
||||||
import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
|
|
||||||
import org.apache.poi.hslf.exceptions.EncryptedPowerPointFileException;
|
|
||||||
import org.apache.poi.hslf.exceptions.OldPowerPointFormatException;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,7 +53,7 @@ public class CurrentUserAtom
|
||||||
public static final byte[] atomHeader = new byte[] { 0, 0, -10, 15 };
|
public static final byte[] atomHeader = new byte[] { 0, 0, -10, 15 };
|
||||||
/** The PowerPoint magic number for a non-encrypted file */
|
/** The PowerPoint magic number for a non-encrypted file */
|
||||||
public static final byte[] headerToken = new byte[] { 95, -64, -111, -29 };
|
public static final byte[] headerToken = new byte[] { 95, -64, -111, -29 };
|
||||||
/** The PowerPoint magic number for an encrytpted file */
|
/** The PowerPoint magic number for an encrypted file */
|
||||||
public static final byte[] encHeaderToken = new byte[] { -33, -60, -47, -13 };
|
public static final byte[] encHeaderToken = new byte[] { -33, -60, -47, -13 };
|
||||||
/** The Powerpoint 97 version, major and minor numbers */
|
/** The Powerpoint 97 version, major and minor numbers */
|
||||||
public static final byte[] ppt97FileVer = new byte[] { 8, 00, -13, 03, 03, 00 };
|
public static final byte[] ppt97FileVer = new byte[] { 8, 00, -13, 03, 03, 00 };
|
||||||
|
@ -66,6 +72,9 @@ public class CurrentUserAtom
|
||||||
|
|
||||||
/** Only correct after reading in or writing out */
|
/** Only correct after reading in or writing out */
|
||||||
private byte[] _contents;
|
private byte[] _contents;
|
||||||
|
|
||||||
|
/** Flag for encryption state of the whole file */
|
||||||
|
private boolean isEncrypted;
|
||||||
|
|
||||||
|
|
||||||
/* ********************* getter/setter follows *********************** */
|
/* ********************* getter/setter follows *********************** */
|
||||||
|
@ -84,6 +93,9 @@ public class CurrentUserAtom
|
||||||
public String getLastEditUsername() { return lastEditUser; }
|
public String getLastEditUsername() { return lastEditUser; }
|
||||||
public void setLastEditUsername(String u) { lastEditUser = u; }
|
public void setLastEditUsername(String u) { lastEditUser = u; }
|
||||||
|
|
||||||
|
public boolean isEncrypted() { return isEncrypted; }
|
||||||
|
public void setEncrypted(boolean isEncrypted) { this.isEncrypted = isEncrypted; }
|
||||||
|
|
||||||
|
|
||||||
/* ********************* real code follows *************************** */
|
/* ********************* real code follows *************************** */
|
||||||
|
|
||||||
|
@ -100,6 +112,7 @@ public class CurrentUserAtom
|
||||||
releaseVersion = 8;
|
releaseVersion = 8;
|
||||||
currentEditOffset = 0;
|
currentEditOffset = 0;
|
||||||
lastEditUser = "Apache POI";
|
lastEditUser = "Apache POI";
|
||||||
|
isEncrypted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,14 +170,10 @@ public class CurrentUserAtom
|
||||||
*/
|
*/
|
||||||
private void init() {
|
private void init() {
|
||||||
// First up is the size, in 4 bytes, which is fixed
|
// First up is the size, in 4 bytes, which is fixed
|
||||||
// Then is the header - check for encrypted
|
// Then is the header
|
||||||
if(_contents[12] == encHeaderToken[0] &&
|
|
||||||
_contents[13] == encHeaderToken[1] &&
|
|
||||||
_contents[14] == encHeaderToken[2] &&
|
|
||||||
_contents[15] == encHeaderToken[3]) {
|
|
||||||
throw new EncryptedPowerPointFileException("The CurrentUserAtom specifies that the document is encrypted");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
isEncrypted = (LittleEndian.getInt(encHeaderToken) == LittleEndian.getInt(_contents,12));
|
||||||
|
|
||||||
// Grab the edit offset
|
// Grab the edit offset
|
||||||
currentEditOffset = LittleEndian.getUInt(_contents,16);
|
currentEditOffset = LittleEndian.getUInt(_contents,16);
|
||||||
|
|
||||||
|
@ -229,7 +238,7 @@ public class CurrentUserAtom
|
||||||
LittleEndian.putInt(_contents,8,20);
|
LittleEndian.putInt(_contents,8,20);
|
||||||
|
|
||||||
// Now the ppt un-encrypted header token (4 bytes)
|
// Now the ppt un-encrypted header token (4 bytes)
|
||||||
System.arraycopy(headerToken,0,_contents,12,4);
|
System.arraycopy((isEncrypted ? encHeaderToken : headerToken),0,_contents,12,4);
|
||||||
|
|
||||||
// Now the current edit offset
|
// Now the current edit offset
|
||||||
LittleEndian.putInt(_contents,16,(int)currentEditOffset);
|
LittleEndian.putInt(_contents,16,(int)currentEditOffset);
|
||||||
|
|
|
@ -17,10 +17,20 @@
|
||||||
|
|
||||||
package org.apache.poi.hslf.record;
|
package org.apache.poi.hslf.record;
|
||||||
|
|
||||||
import org.apache.poi.util.StringUtil;
|
import java.io.ByteArrayInputStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.Hashtable;
|
||||||
|
|
||||||
|
import org.apache.poi.poifs.crypt.CipherAlgorithm;
|
||||||
|
import org.apache.poi.poifs.crypt.EncryptionInfo;
|
||||||
|
import org.apache.poi.poifs.crypt.EncryptionMode;
|
||||||
|
import org.apache.poi.poifs.crypt.HashAlgorithm;
|
||||||
|
import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionHeader;
|
||||||
|
import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptionVerifier;
|
||||||
|
import org.apache.poi.util.LittleEndian;
|
||||||
|
import org.apache.poi.util.LittleEndianByteArrayOutputStream;
|
||||||
|
import org.apache.poi.util.LittleEndianInputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Document Encryption Atom (type 12052). Holds information
|
* A Document Encryption Atom (type 12052). Holds information
|
||||||
|
@ -28,56 +38,64 @@ import java.io.OutputStream;
|
||||||
*
|
*
|
||||||
* @author Nick Burch
|
* @author Nick Burch
|
||||||
*/
|
*/
|
||||||
public final class DocumentEncryptionAtom extends RecordAtom {
|
public final class DocumentEncryptionAtom extends PositionDependentRecordAtom {
|
||||||
|
private static long _type = 12052l;
|
||||||
private byte[] _header;
|
private byte[] _header;
|
||||||
private static long _type = 12052l;
|
private EncryptionInfo ei;
|
||||||
|
|
||||||
private byte[] data;
|
|
||||||
private String encryptionProviderName;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For the Document Encryption Atom
|
* For the Document Encryption Atom
|
||||||
*/
|
*/
|
||||||
protected DocumentEncryptionAtom(byte[] source, int start, int len) {
|
protected DocumentEncryptionAtom(byte[] source, int start, int len) throws IOException {
|
||||||
// Get the header
|
// Get the header
|
||||||
_header = new byte[8];
|
_header = new byte[8];
|
||||||
System.arraycopy(source,start,_header,0,8);
|
System.arraycopy(source,start,_header,0,8);
|
||||||
|
|
||||||
// Grab everything else, for now
|
ByteArrayInputStream bis = new ByteArrayInputStream(source, start+8, len-8);
|
||||||
data = new byte[len-8];
|
LittleEndianInputStream leis = new LittleEndianInputStream(bis);
|
||||||
System.arraycopy(source, start+8, data, 0, len-8);
|
ei = new EncryptionInfo(leis, true);
|
||||||
|
|
||||||
// Grab the provider, from byte 8+44 onwards
|
|
||||||
// It's a null terminated Little Endian String
|
|
||||||
int endPos = -1;
|
|
||||||
int pos = start + 8+44;
|
|
||||||
while(pos < (start+len) && endPos < 0) {
|
|
||||||
if(source[pos] == 0 && source[pos+1] == 0) {
|
|
||||||
// Hit the end
|
|
||||||
endPos = pos;
|
|
||||||
}
|
|
||||||
pos += 2;
|
|
||||||
}
|
|
||||||
pos = start + 8+44;
|
|
||||||
int stringLen = (endPos-pos) / 2;
|
|
||||||
encryptionProviderName = StringUtil.getFromUnicodeLE(source, pos, stringLen);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DocumentEncryptionAtom() {
|
||||||
|
_header = new byte[8];
|
||||||
|
LittleEndian.putShort(_header, 0, (short)0x000F);
|
||||||
|
LittleEndian.putShort(_header, 2, (short)_type);
|
||||||
|
// record length not yet known ...
|
||||||
|
|
||||||
|
ei = new EncryptionInfo(EncryptionMode.cryptoAPI);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the encryption settings
|
||||||
|
*
|
||||||
|
* @param keyBits see {@link CipherAlgorithm#rc4} for allowed values, use -1 for default size
|
||||||
|
*/
|
||||||
|
public void initializeEncryptionInfo(int keyBits) {
|
||||||
|
ei = new EncryptionInfo(EncryptionMode.cryptoAPI, CipherAlgorithm.rc4, HashAlgorithm.sha1, keyBits, -1, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the length of the encryption key, in bits
|
* Return the length of the encryption key, in bits
|
||||||
*/
|
*/
|
||||||
public int getKeyLength() {
|
public int getKeyLength() {
|
||||||
return data[28];
|
return ei.getHeader().getKeySize();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the name of the encryption provider used
|
* Return the name of the encryption provider used
|
||||||
*/
|
*/
|
||||||
public String getEncryptionProviderName() {
|
public String getEncryptionProviderName() {
|
||||||
return encryptionProviderName;
|
return ei.getHeader().getCspName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the {@link EncryptionInfo} object for details about encryption settings
|
||||||
|
*/
|
||||||
|
public EncryptionInfo getEncryptionInfo() {
|
||||||
|
return ei;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We are of type 12052
|
* We are of type 12052
|
||||||
*/
|
*/
|
||||||
|
@ -88,10 +106,24 @@ public final class DocumentEncryptionAtom extends RecordAtom {
|
||||||
* to disk
|
* to disk
|
||||||
*/
|
*/
|
||||||
public void writeOut(OutputStream out) throws IOException {
|
public void writeOut(OutputStream out) throws IOException {
|
||||||
// Header
|
|
||||||
out.write(_header);
|
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
out.write(data);
|
byte data[] = new byte[1024];
|
||||||
|
LittleEndianByteArrayOutputStream bos = new LittleEndianByteArrayOutputStream(data, 0);
|
||||||
|
bos.writeShort(ei.getVersionMajor());
|
||||||
|
bos.writeShort(ei.getVersionMinor());
|
||||||
|
bos.writeInt(ei.getEncryptionFlags());
|
||||||
|
|
||||||
|
((CryptoAPIEncryptionHeader)ei.getHeader()).write(bos);
|
||||||
|
((CryptoAPIEncryptionVerifier)ei.getVerifier()).write(bos);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
LittleEndian.putInt(_header, 4, bos.getWriteIndex());
|
||||||
|
out.write(_header);
|
||||||
|
out.write(data, 0, bos.getWriteIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateOtherRecordReferences(Hashtable<Integer,Integer> oldToNewReferencesLookup) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,17 @@
|
||||||
|
|
||||||
package org.apache.poi.hslf.record;
|
package org.apache.poi.hslf.record;
|
||||||
|
|
||||||
import org.apache.poi.util.LittleEndian;
|
import java.io.ByteArrayOutputStream;
|
||||||
import org.apache.poi.util.POILogger;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.Hashtable;
|
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
|
* 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
|
* that knows about a given slide to find the right location
|
||||||
*/
|
*/
|
||||||
private Hashtable<Integer,Integer> _slideLocations;
|
private Hashtable<Integer,Integer> _slideLocations;
|
||||||
/**
|
|
||||||
* Holds the lookup from slide id to where their offset is
|
private static final BitField persistIdFld = new BitField(0X000FFFFF);
|
||||||
* held inside _ptrData. Used when writing out, and updating
|
private static final BitField cntPersistFld = new BitField(0XFFF00000);
|
||||||
* the positions of the slides
|
|
||||||
*/
|
/**
|
||||||
private Hashtable<Integer,Integer> _slideOffsetDataLocation;
|
* 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.
|
* Get the list of slides that this PersistPtrHolder knows about.
|
||||||
|
@ -63,10 +69,9 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom
|
||||||
*/
|
*/
|
||||||
public int[] getKnownSlideIDs() {
|
public int[] getKnownSlideIDs() {
|
||||||
int[] ids = new int[_slideLocations.size()];
|
int[] ids = new int[_slideLocations.size()];
|
||||||
Enumeration<Integer> e = _slideLocations.keys();
|
int i = 0;
|
||||||
for(int i=0; i<ids.length; i++) {
|
for (Integer slideId : _slideLocations.keySet()) {
|
||||||
Integer id = e.nextElement();
|
ids[i++] = slideId;
|
||||||
ids[i] = id.intValue();
|
|
||||||
}
|
}
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
@ -78,46 +83,16 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom
|
||||||
public Hashtable<Integer,Integer> getSlideLocationsLookup() {
|
public Hashtable<Integer,Integer> getSlideLocationsLookup() {
|
||||||
return _slideLocations;
|
return _slideLocations;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the lookup from slide numbers to their offsets inside
|
* Get the lookup from slide numbers to their offsets inside
|
||||||
* _ptrData, used when adding or moving slides.
|
* _ptrData, used when adding or moving slides.
|
||||||
|
*
|
||||||
|
* @deprecated since POI 3.11, not supported anymore
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public Hashtable<Integer,Integer> getSlideOffsetDataLocationsLookup() {
|
public Hashtable<Integer,Integer> getSlideOffsetDataLocationsLookup() {
|
||||||
return _slideOffsetDataLocation;
|
throw new UnsupportedOperationException("PersistPtrHolder.getSlideOffsetDataLocationsLookup() is not supported since 3.12-Beta1");
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -141,30 +116,27 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom
|
||||||
// count * 32 bit offsets
|
// count * 32 bit offsets
|
||||||
// Repeat as many times as you have data
|
// Repeat as many times as you have data
|
||||||
_slideLocations = new Hashtable<Integer,Integer>();
|
_slideLocations = new Hashtable<Integer,Integer>();
|
||||||
_slideOffsetDataLocation = new Hashtable<Integer,Integer>();
|
|
||||||
_ptrData = new byte[len-8];
|
_ptrData = new byte[len-8];
|
||||||
System.arraycopy(source,start+8,_ptrData,0,_ptrData.length);
|
System.arraycopy(source,start+8,_ptrData,0,_ptrData.length);
|
||||||
|
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
while(pos < _ptrData.length) {
|
while(pos < _ptrData.length) {
|
||||||
// Grab the info field
|
// Grab the info field
|
||||||
long info = LittleEndian.getUInt(_ptrData,pos);
|
int info = LittleEndian.getInt(_ptrData,pos);
|
||||||
|
|
||||||
// First 20 bits = offset number
|
// First 20 bits = offset number
|
||||||
// Remaining 12 bits = offset count
|
// Remaining 12 bits = offset count
|
||||||
int offset_count = (int)(info >> 20);
|
int offset_no = persistIdFld.getValue(info);
|
||||||
int offset_no = (int)(info - (offset_count << 20));
|
int offset_count = cntPersistFld.getValue(info);
|
||||||
//System.out.println("Info is " + info + ", count is " + offset_count + ", number is " + offset_no);
|
|
||||||
|
|
||||||
// Wind on by the 4 byte info header
|
// Wind on by the 4 byte info header
|
||||||
pos += 4;
|
pos += 4;
|
||||||
|
|
||||||
// Grab the offsets for each of the sheets
|
// Grab the offsets for each of the sheets
|
||||||
for(int i=0; i<offset_count; i++) {
|
for(int i=0; i<offset_count; i++) {
|
||||||
int sheet_no = offset_no + i;
|
int sheet_no = offset_no + i;
|
||||||
long sheet_offset = LittleEndian.getUInt(_ptrData,pos);
|
int sheet_offset = (int)LittleEndian.getUInt(_ptrData,pos);
|
||||||
_slideLocations.put(Integer.valueOf(sheet_no), Integer.valueOf((int)sheet_offset));
|
_slideLocations.put(sheet_no, sheet_offset);
|
||||||
_slideOffsetDataLocation.put(Integer.valueOf(sheet_no), Integer.valueOf(pos));
|
|
||||||
|
|
||||||
// Wind on by 4 bytes per sheet found
|
// Wind on by 4 bytes per sheet found
|
||||||
pos += 4;
|
pos += 4;
|
||||||
|
@ -172,48 +144,108 @@ public final class PersistPtrHolder extends PositionDependentRecordAtom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the value we were given at creation, be it 6001 or 6002
|
* remove all slide references
|
||||||
*/
|
*
|
||||||
public long getRecordType() { return _type; }
|
* Convenience method provided, for easier reviewing of invocations
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
_slideLocations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new slide, notes or similar, to be looked up by this.
|
||||||
|
*/
|
||||||
|
public void addSlideLookup(int slideID, int posOnDisk) {
|
||||||
|
if (_slideLocations.containsKey(slideID)) {
|
||||||
|
throw new CorruptPowerPointFileException("A record with persistId "+slideID+" already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_slideLocations.put(slideID, posOnDisk);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* At write-out time, update the references to the sheets to their
|
* At write-out time, update the references to the sheets to their
|
||||||
* new positions
|
* new positions
|
||||||
*/
|
*/
|
||||||
public void updateOtherRecordReferences(Hashtable<Integer,Integer> oldToNewReferencesLookup) {
|
public void updateOtherRecordReferences(Hashtable<Integer,Integer> oldToNewReferencesLookup) {
|
||||||
int[] slideIDs = getKnownSlideIDs();
|
|
||||||
|
|
||||||
// Loop over all the slides we know about
|
// Loop over all the slides we know about
|
||||||
// Find where they used to live, and where they now live
|
// Find where they used to live, and where they now live
|
||||||
// Then, update the right bit of _ptrData with their new location
|
for (Map.Entry<Integer,Integer> me : _slideLocations.entrySet()) {
|
||||||
for(int i=0; i<slideIDs.length; i++) {
|
Integer oldPos = me.getValue();
|
||||||
Integer id = Integer.valueOf(slideIDs[i]);
|
Integer newPos = oldToNewReferencesLookup.get(oldPos);
|
||||||
Integer oldPos = (Integer)_slideLocations.get(id);
|
|
||||||
Integer newPos = (Integer)oldToNewReferencesLookup.get(oldPos);
|
|
||||||
|
|
||||||
if(newPos == null) {
|
if (newPos == null) {
|
||||||
logger.log(POILogger.WARN, "Couldn't find the new location of the \"slide\" with id " + id + " that used to be at " + oldPos);
|
Integer id = me.getKey();
|
||||||
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!)");
|
logger.log(POILogger.WARN, "Couldn't find the new location of the \"slide\" with id " + id + " that used to be at " + oldPos);
|
||||||
newPos = 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);
|
||||||
// Write out the new location
|
}
|
||||||
Integer dataOffset = (Integer)_slideOffsetDataLocation.get(id);
|
}
|
||||||
LittleEndian.putInt(_ptrData,dataOffset.intValue(),newPos.intValue());
|
|
||||||
|
|
||||||
// Update our hashtable
|
|
||||||
_slideLocations.remove(id);
|
|
||||||
_slideLocations.put(id,newPos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void normalizePersistDirectory() {
|
||||||
|
TreeMap<Integer,Integer> orderedSlideLocations = new TreeMap<Integer,Integer>(_slideLocations);
|
||||||
|
|
||||||
|
@SuppressWarnings("resource")
|
||||||
|
BufAccessBAOS bos = new BufAccessBAOS();
|
||||||
|
byte intbuf[] = new byte[4];
|
||||||
|
int lastPersistEntry = -1;
|
||||||
|
int lastSlideId = -1;
|
||||||
|
for (Map.Entry<Integer,Integer> me : orderedSlideLocations.entrySet()) {
|
||||||
|
int nextSlideId = me.getKey();
|
||||||
|
int offset = me.getValue();
|
||||||
|
try {
|
||||||
|
// Building the info block
|
||||||
|
// First 20 bits = offset number = slide ID (persistIdFld, i.e. first slide ID of a continuous group)
|
||||||
|
// Remaining 12 bits = offset count = 1 (cntPersistFld, i.e. continuous entries in a group)
|
||||||
|
|
||||||
|
if (lastSlideId+1 == nextSlideId) {
|
||||||
|
// use existing PersistDirectoryEntry, need to increase entry count
|
||||||
|
assert(lastPersistEntry != -1);
|
||||||
|
int infoBlock = LittleEndian.getInt(bos.getBuf(), lastPersistEntry);
|
||||||
|
int entryCnt = cntPersistFld.getValue(infoBlock);
|
||||||
|
infoBlock = cntPersistFld.setValue(infoBlock, entryCnt+1);
|
||||||
|
LittleEndian.putInt(bos.getBuf(), lastPersistEntry, infoBlock);
|
||||||
|
} else {
|
||||||
|
// start new PersistDirectoryEntry
|
||||||
|
lastPersistEntry = bos.size();
|
||||||
|
int infoBlock = persistIdFld.setValue(0, nextSlideId);
|
||||||
|
infoBlock = cntPersistFld.setValue(infoBlock, 1);
|
||||||
|
LittleEndian.putInt(intbuf, 0, infoBlock);
|
||||||
|
bos.write(intbuf);
|
||||||
|
}
|
||||||
|
// Add to the ptrData offset lookup hash
|
||||||
|
LittleEndian.putInt(intbuf, 0, offset);
|
||||||
|
bos.write(intbuf);
|
||||||
|
lastSlideId = nextSlideId;
|
||||||
|
} catch (IOException e) {
|
||||||
|
// ByteArrayOutputStream is very unlikely throwing a IO exception (maybe because of OOM ...)
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the new ptr data
|
||||||
|
_ptrData = bos.toByteArray();
|
||||||
|
|
||||||
|
// Update the atom header
|
||||||
|
LittleEndian.putInt(_header,4,bos.size());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write the contents of the record back, so it can be written
|
* Write the contents of the record back, so it can be written
|
||||||
* to disk
|
* to disk
|
||||||
*/
|
*/
|
||||||
public void writeOut(OutputStream out) throws IOException {
|
public void writeOut(OutputStream out) throws IOException {
|
||||||
|
normalizePersistDirectory();
|
||||||
out.write(_header);
|
out.write(_header);
|
||||||
out.write(_ptrData);
|
out.write(_ptrData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class BufAccessBAOS extends ByteArrayOutputStream {
|
||||||
|
public byte[] getBuf() {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ public abstract class Record
|
||||||
*/
|
*/
|
||||||
public static void writeLittleEndian(int i,OutputStream o) throws IOException {
|
public static void writeLittleEndian(int i,OutputStream o) throws IOException {
|
||||||
byte[] bi = new byte[4];
|
byte[] bi = new byte[4];
|
||||||
LittleEndian.putInt(bi,i);
|
LittleEndian.putInt(bi,0,i);
|
||||||
o.write(bi);
|
o.write(bi);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -82,7 +82,7 @@ public abstract class Record
|
||||||
*/
|
*/
|
||||||
public static void writeLittleEndian(short s,OutputStream o) throws IOException {
|
public static void writeLittleEndian(short s,OutputStream o) throws IOException {
|
||||||
byte[] bs = new byte[2];
|
byte[] bs = new byte[2];
|
||||||
LittleEndian.putShort(bs,s);
|
LittleEndian.putShort(bs,0,s);
|
||||||
o.write(bs);
|
o.write(bs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
package org.apache.poi.hslf.record;
|
package org.apache.poi.hslf.record;
|
||||||
|
|
||||||
import org.apache.poi.util.LittleEndian;
|
import org.apache.poi.util.LittleEndian;
|
||||||
|
import org.apache.poi.util.LittleEndianConsts;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Hashtable;
|
import java.util.Hashtable;
|
||||||
|
@ -42,7 +44,7 @@ public final class UserEditAtom extends PositionDependentRecordAtom
|
||||||
|
|
||||||
private byte[] _header;
|
private byte[] _header;
|
||||||
private static long _type = 4085l;
|
private static long _type = 4085l;
|
||||||
private byte[] reserved;
|
private short unused;
|
||||||
|
|
||||||
private int lastViewedSlideID;
|
private int lastViewedSlideID;
|
||||||
private int pptVersion;
|
private int pptVersion;
|
||||||
|
@ -51,6 +53,7 @@ public final class UserEditAtom extends PositionDependentRecordAtom
|
||||||
private int docPersistRef;
|
private int docPersistRef;
|
||||||
private int maxPersistWritten;
|
private int maxPersistWritten;
|
||||||
private short lastViewType;
|
private short lastViewType;
|
||||||
|
private int encryptSessionPersistIdRef = -1;
|
||||||
|
|
||||||
// Somewhat user facing getters
|
// Somewhat user facing getters
|
||||||
public int getLastViewedSlideID() { return lastViewedSlideID; }
|
public int getLastViewedSlideID() { return lastViewedSlideID; }
|
||||||
|
@ -61,12 +64,17 @@ public final class UserEditAtom extends PositionDependentRecordAtom
|
||||||
public int getPersistPointersOffset() { return persistPointersOffset; }
|
public int getPersistPointersOffset() { return persistPointersOffset; }
|
||||||
public int getDocPersistRef() { return docPersistRef; }
|
public int getDocPersistRef() { return docPersistRef; }
|
||||||
public int getMaxPersistWritten() { return maxPersistWritten; }
|
public int getMaxPersistWritten() { return maxPersistWritten; }
|
||||||
|
public int getEncryptSessionPersistIdRef() { return encryptSessionPersistIdRef; }
|
||||||
|
|
||||||
// More scary internal setters
|
// More scary internal setters
|
||||||
public void setLastUserEditAtomOffset(int offset) { lastUserEditAtomOffset = offset; }
|
public void setLastUserEditAtomOffset(int offset) { lastUserEditAtomOffset = offset; }
|
||||||
public void setPersistPointersOffset(int offset) { persistPointersOffset = offset; }
|
public void setPersistPointersOffset(int offset) { persistPointersOffset = offset; }
|
||||||
public void setLastViewType(short type) { lastViewType=type; }
|
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 ********************** */
|
/* *************** record code follows ********************** */
|
||||||
|
|
||||||
|
@ -77,39 +85,56 @@ public final class UserEditAtom extends PositionDependentRecordAtom
|
||||||
// Sanity Checking
|
// Sanity Checking
|
||||||
if(len < 34) { len = 34; }
|
if(len < 34) { len = 34; }
|
||||||
|
|
||||||
|
int offset = start;
|
||||||
// Get the header
|
// Get the header
|
||||||
_header = new byte[8];
|
_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
|
// 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
|
// 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
|
// Get the offset to the previous incremental save's UserEditAtom
|
||||||
// This will be the byte offset on disk where the previous one
|
// This will be the byte offset on disk where the previous one
|
||||||
// starts, or 0 if this is the first 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
|
// Get the offset to the persist pointers
|
||||||
// This will be the byte offset on disk where the preceding
|
// This will be the byte offset on disk where the preceding
|
||||||
// PersistPtrFullBlock or PersistPtrIncrementalBlock starts
|
// 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
|
// Get the persist reference for the document persist object
|
||||||
// Normally seems to be 1
|
// 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
|
// 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
|
// 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
|
// There might be a few more bytes, which are a reserved field
|
||||||
reserved = new byte[len-26-8];
|
if (offset-start<len) {
|
||||||
System.arraycopy(source,start+26+8,reserved,0,reserved.length);
|
encryptSessionPersistIdRef = LittleEndian.getInt(source,offset);
|
||||||
|
offset += LittleEndianConsts.INT_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(offset-start == len);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -155,8 +180,10 @@ public final class UserEditAtom extends PositionDependentRecordAtom
|
||||||
writeLittleEndian(docPersistRef,out);
|
writeLittleEndian(docPersistRef,out);
|
||||||
writeLittleEndian(maxPersistWritten,out);
|
writeLittleEndian(maxPersistWritten,out);
|
||||||
writeLittleEndian(lastViewType,out);
|
writeLittleEndian(lastViewType,out);
|
||||||
|
writeLittleEndian(unused,out);
|
||||||
// Reserved fields
|
if (encryptSessionPersistIdRef != -1) {
|
||||||
out.write(reserved);
|
// optional field
|
||||||
|
writeLittleEndian(encryptSessionPersistIdRef,out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,56 +17,54 @@
|
||||||
|
|
||||||
package org.apache.poi.hslf.record;
|
package org.apache.poi.hslf.record;
|
||||||
|
|
||||||
import junit.framework.Test;
|
import org.junit.runner.RunWith;
|
||||||
import junit.framework.TestSuite;
|
import org.junit.runners.Suite;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collects all tests from the package <tt>org.apache.poi.hslf.record</tt>.
|
* Collects all tests from the package <tt>org.apache.poi.hslf.record</tt>.
|
||||||
*
|
*
|
||||||
* @author Josh Micich
|
* @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 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,36 +17,33 @@
|
||||||
|
|
||||||
package org.apache.poi.hslf.record;
|
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.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
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.hslf.exceptions.EncryptedPowerPointFileException;
|
||||||
import org.apache.poi.poifs.filesystem.DocumentEntry;
|
import org.apache.poi.poifs.filesystem.DocumentEntry;
|
||||||
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
||||||
import org.apache.poi.POIDataSamples;
|
import org.junit.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests that CurrentUserAtom works properly.
|
* Tests that CurrentUserAtom works properly.
|
||||||
*
|
*
|
||||||
* @author Nick Burch (nick at torchbox dot com)
|
* @author Nick Burch (nick at torchbox dot com)
|
||||||
*/
|
*/
|
||||||
public final class TestCurrentUserAtom extends TestCase {
|
public final class TestCurrentUserAtom {
|
||||||
private static POIDataSamples _slTests = POIDataSamples.getSlideShowInstance();
|
private static POIDataSamples _slTests = POIDataSamples.getSlideShowInstance();
|
||||||
/** Not encrypted */
|
/** Not encrypted */
|
||||||
private String normalFile;
|
private static final String normalFile = "basic_test_ppt_file.ppt";
|
||||||
/** Encrypted */
|
/** Encrypted */
|
||||||
private String encFile;
|
private static final String encFile = "Password_Protected-hello.ppt";
|
||||||
|
|
||||||
protected void setUp() throws Exception {
|
@Test
|
||||||
super.setUp();
|
public void readNormal() throws Exception {
|
||||||
|
|
||||||
normalFile = "basic_test_ppt_file.ppt";
|
|
||||||
encFile = "Password_Protected-hello.ppt";
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testReadNormal() throws Exception {
|
|
||||||
POIFSFileSystem fs = new POIFSFileSystem(
|
POIFSFileSystem fs = new POIFSFileSystem(
|
||||||
_slTests.openResourceAsStream(normalFile)
|
_slTests.openResourceAsStream(normalFile)
|
||||||
);
|
);
|
||||||
|
@ -66,20 +63,20 @@ public final class TestCurrentUserAtom extends TestCase {
|
||||||
assertEquals(0x2942, cu2.getCurrentEditOffset());
|
assertEquals(0x2942, cu2.getCurrentEditOffset());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testReadEnc() throws Exception {
|
@Test(expected = EncryptedPowerPointFileException.class)
|
||||||
|
public void readEnc() throws Exception {
|
||||||
POIFSFileSystem fs = new POIFSFileSystem(
|
POIFSFileSystem fs = new POIFSFileSystem(
|
||||||
_slTests.openResourceAsStream(encFile)
|
_slTests.openResourceAsStream(encFile)
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
new CurrentUserAtom(fs);
|
||||||
new CurrentUserAtom(fs);
|
assertTrue(true); // not yet failed
|
||||||
fail();
|
|
||||||
} catch(EncryptedPowerPointFileException e) {
|
new HSLFSlideShow(fs);
|
||||||
// Good
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testWriteNormal() throws Exception {
|
@Test
|
||||||
|
public void writeNormal() throws Exception {
|
||||||
// Get raw contents from a known file
|
// Get raw contents from a known file
|
||||||
POIFSFileSystem fs = new POIFSFileSystem(
|
POIFSFileSystem fs = new POIFSFileSystem(
|
||||||
_slTests.openResourceAsStream(normalFile)
|
_slTests.openResourceAsStream(normalFile)
|
||||||
|
|
|
@ -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<picsExpected.length; i++) {
|
||||||
|
assertArrayEquals(picsExpected[i].getRawData(), picsActual[i].getRawData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cryptoAPIEncryption() throws Exception {
|
||||||
|
/* documents with multiple edits need to be normalized for encryption */
|
||||||
|
String pptFile = "57272_corrupted_usereditatom.ppt";
|
||||||
|
NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile(pptFile), true);
|
||||||
|
HSLFSlideShow hss = new HSLFSlideShow(fs);
|
||||||
|
hss.normalizeRecords();
|
||||||
|
|
||||||
|
// normalized ppt
|
||||||
|
ByteArrayOutputStream expected = new ByteArrayOutputStream();
|
||||||
|
hss.write(expected);
|
||||||
|
|
||||||
|
// encrypted
|
||||||
|
Biff8EncryptionKey.setCurrentUserPassword("hello");
|
||||||
|
ByteArrayOutputStream encrypted = new ByteArrayOutputStream();
|
||||||
|
hss.write(encrypted);
|
||||||
|
fs.close();
|
||||||
|
|
||||||
|
// decrypted
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(encrypted.toByteArray());
|
||||||
|
fs = new NPOIFSFileSystem(bis);
|
||||||
|
hss = new HSLFSlideShow(fs);
|
||||||
|
Biff8EncryptionKey.setCurrentUserPassword(null);
|
||||||
|
ByteArrayOutputStream actual = new ByteArrayOutputStream();
|
||||||
|
hss.write(actual);
|
||||||
|
fs.close();
|
||||||
|
|
||||||
|
assertArrayEquals(expected.toByteArray(), actual.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cryptoAPIDecryption() throws Exception {
|
||||||
|
// taken from a msdn blog:
|
||||||
|
// http://blogs.msdn.com/b/openspecification/archive/2009/05/08/dominic-salemno.aspx
|
||||||
|
Biff8EncryptionKey.setCurrentUserPassword("crypto");
|
||||||
|
NPOIFSFileSystem fs = new NPOIFSFileSystem(slTests.getFile("cryptoapi-proc2356.ppt"));
|
||||||
|
HSLFSlideShow hss = new HSLFSlideShow(fs);
|
||||||
|
SlideShow ss = new SlideShow(hss);
|
||||||
|
|
||||||
|
Slide slide = ss.getSlides()[0];
|
||||||
|
assertEquals("Dominic Salemno", slide.getTextRuns()[0].getText());
|
||||||
|
|
||||||
|
String picCmp[][] = {
|
||||||
|
{"0","nKsDTKqxTCR8LFkVVWlP9GSTvZ0="},
|
||||||
|
{"95163","SuNOR+9V1UVYZIoeD65l3VTaLoc="},
|
||||||
|
{"100864","Ql3IGrr4bNq07ZTp5iPg7b+pva8="},
|
||||||
|
{"714114","8pdst9NjBGSfWezSZE8+aVhIRe0="},
|
||||||
|
{"723752","go6xqW7lvkCtlOO5tYLiMfb4oxw="},
|
||||||
|
{"770128","gZUM8YqRNL5kGNfyyYvEEernvCc="},
|
||||||
|
{"957958","CNU2iiqUFAnk3TDXsXV1ihH9eRM="},
|
||||||
|
};
|
||||||
|
|
||||||
|
MessageDigest md = CryptoFunctions.getMessageDigest(HashAlgorithm.sha1);
|
||||||
|
PictureData pd[] = hss.getPictures();
|
||||||
|
int i = 0;
|
||||||
|
for (PictureData p : pd) {
|
||||||
|
byte hash[] = md.digest(p.getData());
|
||||||
|
assertEquals(Integer.parseInt(picCmp[i][0]), p.getOffset());
|
||||||
|
assertEquals(picCmp[i][1], Base64.encodeBase64String(hash));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentEncryptionAtom dea = hss.getDocumentEncryptionAtom();
|
||||||
|
|
||||||
|
POIFSFileSystem fs2 = new POIFSFileSystem(dea.getEncryptionInfo().getDecryptor().getDataStream(fs));
|
||||||
|
PropertySet ps = PropertySetFactory.create(fs2.getRoot(), SummaryInformation.DEFAULT_STREAM_NAME);
|
||||||
|
assertTrue(ps.isSummaryInformation());
|
||||||
|
assertEquals("RC4 CryptoAPI Encryption", ps.getProperties()[1].getValue());
|
||||||
|
ps = PropertySetFactory.create(fs2.getRoot(), DocumentSummaryInformation.DEFAULT_STREAM_NAME);
|
||||||
|
assertTrue(ps.isDocumentSummaryInformation());
|
||||||
|
assertEquals("On-screen Show (4:3)", ps.getProperties()[1].getValue());
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,14 +18,18 @@
|
||||||
package org.apache.poi.hslf.record;
|
package org.apache.poi.hslf.record;
|
||||||
|
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests that DocumentEncryptionAtom works properly.
|
* Tests that DocumentEncryptionAtom works properly.
|
||||||
*
|
*
|
||||||
* @author Nick Burch (nick at torchbox dot com)
|
* @author Nick Burch (nick at torchbox dot com)
|
||||||
*/
|
*/
|
||||||
public final class TestDocumentEncryptionAtom extends TestCase {
|
public final class TestDocumentEncryptionAtom {
|
||||||
// From a real file
|
// From a real file
|
||||||
private byte[] data_a = new byte[] {
|
private byte[] data_a = new byte[] {
|
||||||
0x0F, 00, 0x14, 0x2F, 0xBE-256, 00, 00, 00,
|
0x0F, 00, 0x14, 0x2F, 0xBE-256, 00, 00, 00,
|
||||||
|
@ -84,7 +88,8 @@ public final class TestDocumentEncryptionAtom extends TestCase {
|
||||||
3, -104, 22, 6, 102, -61, -98, 62, 40, 61, 21
|
3, -104, 22, 6, 102, -61, -98, 62, 40, 61, 21
|
||||||
};
|
};
|
||||||
|
|
||||||
public void testRecordType() {
|
@Test
|
||||||
|
public void recordType() throws IOException {
|
||||||
DocumentEncryptionAtom dea1 = new DocumentEncryptionAtom(data_a, 0, data_a.length);
|
DocumentEncryptionAtom dea1 = new DocumentEncryptionAtom(data_a, 0, data_a.length);
|
||||||
assertEquals(12052l, dea1.getRecordType());
|
assertEquals(12052l, dea1.getRecordType());
|
||||||
|
|
||||||
|
@ -95,7 +100,8 @@ public final class TestDocumentEncryptionAtom extends TestCase {
|
||||||
assertEquals(198, data_b.length);
|
assertEquals(198, data_b.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testEncryptionTypeName() {
|
@Test
|
||||||
|
public void encryptionTypeName() throws IOException {
|
||||||
DocumentEncryptionAtom dea1 = new DocumentEncryptionAtom(data_a, 0, data_a.length);
|
DocumentEncryptionAtom dea1 = new DocumentEncryptionAtom(data_a, 0, data_a.length);
|
||||||
assertEquals("Microsoft Base Cryptographic Provider v1.0", dea1.getEncryptionProviderName());
|
assertEquals("Microsoft Base Cryptographic Provider v1.0", dea1.getEncryptionProviderName());
|
||||||
|
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue