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