0
votes

like the JDK Deflater/Inflater classes that allows to pass byte[] chunks and get the compressed/uncompressed value as a byte[] chunk also (No need for Input or Output Streams), does anyone know of a way to do the same but for Zip files? The idea is to be able to read an input stream by chunks and do a kind of transformation pipeline: - Inbound: Encrypt and compress - Outbound: Decrypt and decompress

With the ZipInput/OutputStream classes in order to do that I need to save all the bytes before encrypting/decrypting.

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

public class Compression {

    public static void main(String[] args) throws IOException, DataFormatException {
        final int bufferSize = 1024;
        byte[] uncompressedChunkBuffer = new byte[bufferSize];
        int uncompressedChunkLength = 0;
        byte[] compressedChunkBuffer = new byte[bufferSize];
        int compressedChunkLength = 0;
        //Compression
        Deflater deflater = new Deflater();
        String uncompressedText = randomText();
        byte[] expectedUncompressedBytes = uncompressedText.getBytes();
        System.out.println("Bytes Length: " + expectedUncompressedBytes.length);
        ByteArrayInputStream uncompressedBytesInStream = new ByteArrayInputStream(expectedUncompressedBytes);
        ByteArrayOutputStream compressedBytesOutStream = new ByteArrayOutputStream();
        while ((uncompressedChunkLength = uncompressedBytesInStream.read(uncompressedChunkBuffer)) != -1) {
            //This part allows to set and get byte[] chunks 
            deflater.setInput(uncompressedChunkBuffer, 0, uncompressedChunkLength);
            while (!deflater.needsInput()) {
                compressedChunkLength = deflater.deflate(compressedChunkBuffer);
                if (compressedChunkLength > 0) {
                    compressedBytesOutStream.write(compressedChunkBuffer, 0, compressedChunkLength);
                }
            }
        }
        deflater.finish();
        while (!deflater.finished()) {
            compressedChunkLength = deflater.deflate(compressedChunkBuffer);
            if (compressedChunkLength > 0) {
                compressedBytesOutStream.write(compressedChunkBuffer, 0, compressedChunkLength);
            }
        }
        deflater.end();
        uncompressedBytesInStream.close();
        compressedBytesOutStream.flush();
        compressedBytesOutStream.close();
        byte[] compressedBytes = compressedBytesOutStream.toByteArray();
        System.out.println("Compressed Bytes Length: " + compressedBytes.length);
        //Decompression
        Inflater inflater = new Inflater();
        ByteArrayInputStream compressedBytesInStream = new ByteArrayInputStream(compressedBytes);
        ByteArrayOutputStream uncompressedBytesOutStream = new ByteArrayOutputStream();
        while ((compressedChunkLength = compressedBytesInStream.read(compressedChunkBuffer)) != -1) {
            //This part allows to set and get byte[] chunks
            inflater.setInput(compressedChunkBuffer, 0, compressedChunkLength);
            while ((uncompressedChunkLength = inflater.inflate(uncompressedChunkBuffer)) > 0) {
                uncompressedBytesOutStream.write(uncompressedChunkBuffer, 0, uncompressedChunkLength);
            }
        }
        while ((uncompressedChunkLength = inflater.inflate(uncompressedChunkBuffer)) > 0) {
            uncompressedBytesOutStream.write(uncompressedChunkBuffer, 0, uncompressedChunkLength);
        }
        inflater.end();
        compressedBytesInStream.close();
        uncompressedBytesOutStream.flush();
        uncompressedBytesOutStream.close();
        byte[] actualUncompressedBytes = uncompressedBytesOutStream.toByteArray();
        System.out.println("Uncompressed Bytes Length: Expected[" + expectedUncompressedBytes.length + "], Actual [" + actualUncompressedBytes.length + "]");
    }

    public static String randomText() {
        StringBuilder sb = new StringBuilder();
        int textLength = rnd(100, 999);
        for (int i = 0; i < textLength; i++) {
            if (rnd(0, 1) == 0) {
                sb.append((char) rnd(65, 90));
            } else {
                sb.append((char) rnd(49, 57));
            }
        }
        return sb.toString();
    }

    public static int rnd(int min, int max) {
        return min + (int) (Math.random() * ((max - min) + 1));
    }
}
1
The whole idea of InputStream and OutputStream are that you can create your own subclasses that wrap other streams. This gives your subclass access to the data as it is being streamed (down to one byte at a time).Rob

1 Answers

0
votes

Thanks to @rob suggestion I finally reached a solution:

private static final String SECRET_KEY_ALGO = "AES";
private static final int SECRET_KEY_SIZE_IN_BITS = 256;
private static final String AES_TRANSFORMATION = "AES/CBC/PKCS5Padding";
private static final int DEFAULT_BUFFERSIZE = 8 * 1024;

public static void main(String[] args) throws IOException {
    String expected = randomText();
    byte[] textBytes = expected.getBytes();
    EncryptedOutputStreamWrapper enc = new EncryptedOutputStreamWrapper();
    {
        InputStream in = new ByteArrayInputStream(textBytes);
        ZipOutputStream out = new ZipOutputStream(enc.wrap(new FileOutputStream("f.zip")));
        out.putNextEntry(new ZipEntry("_"));
        IOUtils.copy(in, out);
        in.close();
        out.closeEntry();
        out.close();
    }
    //
    DecryptedInputStreamWrapper dec = new DecryptedInputStreamWrapper(enc.getSKey(), enc.getIv());
    {
        ZipInputStream in = new ZipInputStream(dec.wrap(new FileInputStream("f.zip")));
        OutputStream out = new FileOutputStream("f.txt");
        in.getNextEntry();
        IOUtils.copy(in, out);
        in.closeEntry();
        in.close();
        out.close();
    }
    //
    String actual = new String(IOUtils.toByteArray(new FileInputStream("f.txt")));
    if (!expected.equals(actual)) {
        System.out.println("Fail!");
        System.out.println("Expected '" + expected + "'");
        System.out.println();
        System.out.println("Actual: '" + actual + "'");
    } else {
        System.out.println("Success!");
    }
}

public static class EncryptedOutputStreamWrapper {
    private Cipher cipher;
    private SecretKey sKey;
    private byte[] iv;

    public EncryptedOutputStreamWrapper() {
        try {
            KeyGenerator generator = KeyGenerator.getInstance(SECRET_KEY_ALGO);
            generator.init(SECRET_KEY_SIZE_IN_BITS);
            this.sKey = generator.generateKey();
            this.cipher = Cipher.getInstance(AES_TRANSFORMATION);
            this.cipher.init(Cipher.ENCRYPT_MODE, sKey);
            this.iv = cipher.getIV();
        } catch (Exception e) {
            throw new CipherException("Error encrypting", e);
        }
    }

    public OutputStream wrap(final OutputStream out) {
        return new BufferedOutputStream(new OutputStream() {
            @Override
            public void write(int b) throws IOException {
            }

            @Override
            public void write(byte[] plainBytes, int off, int len) throws IOException {
                byte[] encryptedBytes = cipher.update(plainBytes, off, len);
                if (encryptedBytes != null) {
                    out.write(encryptedBytes, 0, encryptedBytes.length);
                }
            }

            @Override
            public void flush() throws IOException {
                out.flush();
            }

            @Override
            public void close() throws IOException {
                try {
                    byte[] encryptedBytes = cipher.doFinal();
                    if (encryptedBytes != null) {
                        out.write(encryptedBytes, 0, encryptedBytes.length);
                    }
                } catch (Exception e) {
                    throw new IOException("Error encrypting", e);
                }
                out.close();
            }
        });
    }

    public SecretKey getSKey() {
        return sKey;
    }

    public byte[] getIv() {
        return iv;
    }

}

public static class DecryptedInputStreamWrapper {
    private Cipher cipher;

    public DecryptedInputStreamWrapper(SecretKey sKey, byte[] iv) {
        try {
            this.cipher = Cipher.getInstance(AES_TRANSFORMATION);
            this.cipher.init(Cipher.DECRYPT_MODE, sKey, new IvParameterSpec(iv));
        } catch (Exception e) {
            throw new CipherException("Error decrypting", e);
        }
    }

    public InputStream wrap(final InputStream in) {
        return new BufferedInputStream(new InputStream() {
            private byte[] buffer = new byte[DEFAULT_BUFFERSIZE];
            private boolean done;

            @Override
            public int read() throws IOException {
                return 0;
            }

            @Override
            public int read(byte[] bytes, int off, int len) throws IOException {
                if (done) {
                    return -1;
                }
                int encryptedLen = in.read(buffer);
                try {
                    byte[] plainBytes = null;
                    if (encryptedLen == -1) {
                        done = true;
                        plainBytes = cipher.doFinal();
                    } else {
                        plainBytes = cipher.update(buffer, 0, encryptedLen);
                    }
                    if (plainBytes != null) {
                        System.arraycopy(plainBytes, 0, bytes, off, plainBytes.length);
                        return plainBytes.length;
                    }
                } catch (Exception e) {
                    throw new IOException("Error decrypting", e);
                }
                return 0;
            }

            @Override
            public void close() throws IOException {
                in.close();
            }

        });
    }
}

public static class CipherException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public CipherException() {
        super();
    }

    public CipherException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

    public CipherException(String message, Throwable cause) {
        super(message, cause);
    }

    public CipherException(String message) {
        super(message);
    }

    public CipherException(Throwable cause) {
        super(cause);
    }

}

public static String randomText() {
    StringBuilder sb = new StringBuilder();
    int textLength = rnd(100000, 999999);
    for (int i = 0; i < textLength; i++) {
        if (rnd(0, 1) == 0) {
            sb.append((char) rnd(65, 90));
        } else {
            sb.append((char) rnd(49, 57));
        }
    }
    return sb.toString();
}

public static int rnd(int min, int max) {
    return min + (int) (Math.random() * ((max - min) + 1));
}