- 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:
Andreas Beeker 2014-12-25 01:56:29 +00:00
parent 2668385b17
commit 0839a097e3
48 changed files with 3235 additions and 914 deletions

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
} }
} }

View File

@ -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;
}
} }

View File

@ -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;

View File

@ -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();
} }

View File

@ -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;

View File

@ -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;
} }

View File

@ -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;
}
}

View File

@ -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) {
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
} }

View File

@ -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);
} }

View File

@ -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);
} }

View File

@ -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);
} }

View File

@ -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);

View File

@ -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;
}
} }

View File

@ -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;
}
} }
} }

View File

@ -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;
} }
} }

View File

@ -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

View File

@ -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);
}
}
} }

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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;
}
} }

View File

@ -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;
}
}
} }

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
} }

View File

@ -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);
}
} }

View File

@ -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);

View File

@ -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) {
}
} }

View File

@ -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;
}
}
} }

View File

@ -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);
} }

View File

@ -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);
}
} }
} }

View File

@ -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;
}
} }

View File

@ -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)

View File

@ -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());
}
}

View File

@ -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.