123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- /* ====================================================================
- 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 static org.apache.poi.poifs.crypt.Decryptor.DEFAULT_POIFS_ENTRY;
-
- 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.BadPaddingException;
- import javax.crypto.Cipher;
- import javax.crypto.IllegalBlockSizeException;
- import javax.crypto.ShortBufferException;
-
- import com.zaxxer.sparsebits.SparseBitSet;
- 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.IOUtils;
- import org.apache.poi.util.Internal;
- import org.apache.poi.util.LittleEndian;
- import org.apache.poi.util.LittleEndianConsts;
- import org.apache.poi.util.POILogFactory;
- import org.apache.poi.util.POILogger;
- import org.apache.poi.util.TempFile;
-
- @Internal
- public abstract class ChunkedCipherOutputStream extends FilterOutputStream {
- private static final POILogger LOG = POILogFactory.getLogger(ChunkedCipherOutputStream.class);
- //arbitrarily selected; may need to increase
- private static final int MAX_RECORD_LENGTH = 100_000;
-
- private static final int STREAMING = -1;
-
- private final int chunkSize;
- private final int chunkBits;
-
- private final byte[] chunk;
- private final SparseBitSet plainByteFlags;
- private final File fileOut;
- private final DirectoryNode dir;
-
- private long pos;
- private long totalPos;
- private long written;
-
- // the cipher can't be final, because for the last chunk we change the padding
- // and therefore need to change the cipher too
- private Cipher cipher;
- private boolean isClosed;
-
- public ChunkedCipherOutputStream(DirectoryNode dir, int chunkSize) throws IOException, GeneralSecurityException {
- super(null);
- this.chunkSize = chunkSize;
- int cs = chunkSize == STREAMING ? 4096 : chunkSize;
- this.chunk = IOUtils.safelyAllocate(cs, MAX_RECORD_LENGTH);
- this.plainByteFlags = new SparseBitSet(cs);
- this.chunkBits = Integer.bitCount(cs-1);
- this.fileOut = TempFile.createTempFile("encrypted_package", "crypt");
- this.fileOut.deleteOnExit();
- this.out = new FileOutputStream(fileOut);
- this.dir = dir;
- this.cipher = initCipherForBlock(null, 0, false);
- }
-
- public ChunkedCipherOutputStream(OutputStream stream, int chunkSize) throws IOException, GeneralSecurityException {
- super(stream);
- this.chunkSize = chunkSize;
- int cs = chunkSize == STREAMING ? 4096 : chunkSize;
- this.chunk = IOUtils.safelyAllocate(cs, MAX_RECORD_LENGTH);
- this.plainByteFlags = new SparseBitSet(cs);
- this.chunkBits = Integer.bitCount(cs-1);
- this.fileOut = null;
- this.dir = null;
- this.cipher = initCipherForBlock(null, 0, false);
- }
-
- public final Cipher initCipherForBlock(int block, boolean lastChunk) throws IOException, GeneralSecurityException {
- return initCipherForBlock(cipher, block, lastChunk);
- }
-
- // helper method to break a recursion loop introduced because of an IBMJCE bug, i.e. not resetting on Cipher.doFinal()
- @Internal
- protected Cipher initCipherForBlockNoFlush(Cipher existing, int block, boolean lastChunk)
- throws IOException, GeneralSecurityException {
- return initCipherForBlock(cipher, block, lastChunk);
- }
-
- protected abstract Cipher initCipherForBlock(Cipher existing, int block, boolean lastChunk)
- throws IOException, GeneralSecurityException;
-
- protected abstract void calculateChecksum(File fileOut, int oleStreamSize)
- throws GeneralSecurityException, IOException;
-
- protected abstract void createEncryptionInfoEntry(DirectoryNode dir, File tmpFile)
- throws IOException, GeneralSecurityException;
-
- @Override
- public void write(int b) throws IOException {
- write(new byte[]{(byte)b});
- }
-
- @Override
- public void write(byte[] b) throws IOException {
- write(b, 0, b.length);
- }
-
- @Override
- public void write(byte[] b, int off, int len) throws IOException {
- write(b, off, len, false);
- }
-
- public void writePlain(byte[] b, int off, int len) throws IOException {
- write(b, off, len, true);
- }
-
- protected void write(byte[] b, int off, int len, boolean writePlain) throws IOException {
- if (len == 0) {
- return;
- }
-
- if (len < 0 || b.length < off+len) {
- throw new IOException("not enough bytes in your input buffer");
- }
-
- final int chunkMask = getChunkMask();
- while (len > 0) {
- int posInChunk = (int)(pos & chunkMask);
- int nextLen = Math.min(chunk.length-posInChunk, len);
- System.arraycopy(b, off, chunk, posInChunk, nextLen);
- if (writePlain) {
- plainByteFlags.set(posInChunk, posInChunk+nextLen);
- }
- pos += nextLen;
- totalPos += nextLen;
- off += nextLen;
- len -= nextLen;
- if ((pos & chunkMask) == 0) {
- writeChunk(len > 0);
- }
- }
- }
-
- protected int getChunkMask() {
- return chunk.length-1;
- }
-
- protected void writeChunk(boolean continued) throws IOException {
- if (pos == 0 || totalPos == written) {
- return;
- }
-
- int posInChunk = (int)(pos & getChunkMask());
-
- // 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 = chunk.length;
- lastChunk = false;
- } else {
- // pad the last chunk
- lastChunk = true;
- }
-
- int ciLen;
- try {
- boolean doFinal = true;
- long oldPos = pos;
- // reset stream (not only) in case we were interrupted by plain stream parts
- // this also needs to be set to prevent an endless loop
- pos = 0;
- if (chunkSize == STREAMING) {
- if (continued) {
- doFinal = false;
- }
- } else {
- cipher = initCipherForBlock(cipher, index, lastChunk);
- // restore pos - only streaming chunks will be reset
- pos = oldPos;
- }
- ciLen = invokeCipher(posInChunk, doFinal);
- } catch (GeneralSecurityException e) {
- throw new IOException("can't re-/initialize cipher", e);
- }
-
- out.write(chunk, 0, ciLen);
- plainByteFlags.clear();
- written += ciLen;
- }
-
- /**
- * Helper function for overriding the cipher invocation, i.e. XOR doesn't use a cipher
- * and uses it's own implementation
- *
- * @throws BadPaddingException
- * @throws IllegalBlockSizeException
- * @throws ShortBufferException
- */
- protected int invokeCipher(int posInChunk, boolean doFinal) throws GeneralSecurityException, IOException {
- byte[] plain = (plainByteFlags.isEmpty()) ? null : chunk.clone();
-
- int ciLen = (doFinal)
- ? cipher.doFinal(chunk, 0, posInChunk, chunk)
- : cipher.update(chunk, 0, posInChunk, chunk);
-
- if (doFinal && "IBMJCE".equals(cipher.getProvider().getName()) && "RC4".equals(cipher.getAlgorithm())) {
- // workaround for IBMs cipher not resetting on doFinal
-
- int index = (int)(pos >> chunkBits);
- boolean lastChunk;
- if (posInChunk==0) {
- index--;
- posInChunk = chunk.length;
- lastChunk = false;
- } else {
- // pad the last chunk
- lastChunk = true;
- }
-
- cipher = initCipherForBlockNoFlush(cipher, index, lastChunk);
- }
-
- if (plain != null) {
- int i = plainByteFlags.nextSetBit(0);
- while (i >= 0 && i < posInChunk) {
- chunk[i] = plain[i];
- i = plainByteFlags.nextSetBit(i+1);
- }
- }
-
- return ciLen;
- }
-
- @Override
- public void close() throws IOException {
- if (isClosed) {
- LOG.log(POILogger.DEBUG, "ChunkedCipherOutputStream was already closed - ignoring");
- return;
- }
-
- isClosed = true;
-
- try {
- writeChunk(false);
-
- super.close();
-
- if (fileOut != null) {
- int oleStreamSize = (int)(fileOut.length()+LittleEndianConsts.LONG_SIZE);
- calculateChecksum(fileOut, (int)pos);
- dir.createDocument(DEFAULT_POIFS_ENTRY, oleStreamSize, new EncryptedPackageWriter());
- createEncryptionInfoEntry(dir, fileOut);
- }
- } catch (GeneralSecurityException e) {
- throw new IOException(e);
- }
- }
-
- protected byte[] getChunk() {
- return chunk;
- }
-
- protected SparseBitSet getPlainByteFlags() {
- return plainByteFlags;
- }
-
- protected long getPos() {
- return pos;
- }
-
- protected long getTotalPos() {
- return totalPos;
- }
-
- /**
- * Some ciphers (actually just XOR) are based on the record size,
- * which needs to be set before encryption
- *
- * @param recordSize the size of the next record
- * @param isPlain {@code true} if the record is unencrypted
- */
- public void setNextRecordSize(int recordSize, boolean isPlain) {
- }
-
- private class EncryptedPackageWriter implements POIFSWriterListener {
- @Override
- public void processPOIFSWriterEvent(POIFSWriterEvent event) {
- try {
- try (OutputStream os = event.getStream();
- FileInputStream fis = new FileInputStream(fileOut)) {
-
- // 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
- byte[] buf = new byte[LittleEndianConsts.LONG_SIZE];
- LittleEndian.putLong(buf, 0, pos);
- os.write(buf);
-
- IOUtils.copy(fis, os);
- }
-
- if (!fileOut.delete()) {
- LOG.log(POILogger.ERROR, "Can't delete temporary encryption file: "+fileOut);
- }
- } catch (IOException e) {
- throw new EncryptedDocumentException(e);
- }
- }
- }
- }
|