260 lines
11 KiB
Java
260 lines
11 KiB
Java
/* ====================================================================
|
|
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;
|
|
}
|
|
}
|