/* ==================================================================== | |
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.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; | |
import org.apache.poi.util.StringUtil; | |
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(StringUtil.getToUnicodeLE(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(); | |
entry.streamName = StringUtil.readUnicodeLE(leis, nameSize); | |
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; | |
} | |
} |