/* ==================================================================== | |
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); | |
} | |
} | |
} | |
} |