Decryption much slower compared to encryption on Android
Asked Answered
H

2

5

I have an implementation of 'AES' encryption and decryption with 'CBC' mode and 'PKCS5Padding' padding in Kotlin. I noticed that while decrypting cipherInputStream.read(buffer) reads only 512 bytes at a time instead of the full buffer size which is 8192 bytes. Why is that? While encrypting it uses whole buffer.

These are the constants that I am using,

private val TRANSFORMATION = "AES/CBC/PKCS5Padding"
private var SECRET_KEY_FAC_ALGORITHM = "PBKDF2WithHmacSHA1"
private val SECRET_KEY_SPEC_ALGORITHM = "AES"

private val cipher = Cipher.getInstance(TRANSFORMATION)
private val random = SecureRandom()

private val KEY_BITS_LENGTH = 256
private val IV_BYTES_LENGTH = cipher.blockSize
private val SALT_BYTES_LENGTH = KEY_BITS_LENGTH / 8
private val ITERATIONS = 10000

Decryption code

cis = CipherInputStream(input, cipher)
val buffer = ByteArray(8192)

var read = cis.read(buffer)
while (read > -1) {
    fos.write(buffer, 0, read)
    read = cis.read(buffer)
}

Encryption code

fos.write(iv)
fos.write(salt)

cos = CipherOutputStream(fos, cipher)
val buffer = ByteArray(8192)

var read = input.read(buffer)
while (read > -1) {
    cos.write(buffer, 0, read)
    read = input.read(buffer)
}
Helmsman answered 9/7, 2017 at 4:32 Comment(4)
Using input.available() is a bad idea, because it might not reach the end of the stream at which point you would produce incomplete plaintexts. Just keep the contents of the else-block and remove the if elseDeth
Updated my implementationHelmsman
You did not show your encryption code. AES decryption is usually a tad bit slower than encryption. Its also not clear what "much slower" means. "... decrypting cipherInputStream.read(buffer) reads only 512 bytes ... instead of the full buffer size ... Why is that?" - Devices are resources constrained, and its not uncommon to "chunk" or "block" data to the size of a disk sector. Using a disk sector allows efficient processing of both memory and file streams. 8192-bytes is not bad, but avoid large allocations. It avoids memory pressures and OS messages like onLowMemory().Eau
@Eau I added encryption code too. I tried to debug the encryption and decryption process and found that while encrypting, read is 8192 that is full buffer size but while decrypting, read is 512 only in-spite of the actual buffer size which is 8192. Why such difference?Helmsman
O
7

Recently I had a similar issue.

The problem was internal buffer of CipherInputStream class which is defined as follows

private byte[] ibuffer = new byte[512];

What significantly improved decryption speed was increasing this buffer size to 8192. So I've just copy pasted original CipherInputStream class to my own class and modified buffer size.

What is funny is the comment above this ibuffer field.

the size 512 bytes is somewhat randomly chosen */

Hope it helped

Octopus answered 21/4, 2018 at 15:37 Comment(0)
R
0

I just implemented the class by changing the size of the length of ibuffer. (Copy paste with the changed value only)

import java.io.IOException;
import java.io.InputStream;

import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NullCipher;
import javax.crypto.ShortBufferException;

public class FasterCipherInputStream extends CipherInputStream {
    private static final String TAG = "FasterCipherInputStream";

    private static final int BUFFER_SIZE = 20971520;
    // the cipher engine to use to process stream data
    private final Cipher cipher;

    // the underlying input stream
    private final InputStream input;

    /* the buffer holding data that have been read in from the
       underlying stream, but have not been processed by the cipher
       engine. the size 512 bytes is somewhat randomly chosen */
    private final byte[] ibuffer = new byte[BUFFER_SIZE];

    // having reached the end of the underlying input stream
    private boolean done = false;

    /* the buffer holding data that have been processed by the cipher
       engine, but have not been read out */
    private byte[] obuffer;
    // the offset pointing to the next "new" byte
    private int ostart = 0;
    // the offset pointing to the last "new" byte
    private int ofinish = 0;
    // stream status
    private boolean closed = false;

    /**
     * private convenience function.
     *
     * Entry condition: ostart = ofinish
     *
     * Exit condition: ostart <= ofinish
     *
     * return (ofinish-ostart) (we have this many bytes for you)
     * return 0 (no data now, but could have more later)
     * return -1 (absolutely no more data)
     *
     * Note:  Exceptions are only thrown after the stream is completely read.
     * For AEAD ciphers a read() of any length will internally cause the
     * whole stream to be read fully and verify the authentication tag before
     * returning decrypted data or exceptions.
     */
    private int getMoreData() throws IOException {
        // Android-changed: The method was creating a new object every time update(byte[], int, int)
        // or doFinal() was called resulting in the old object being GCed. With do(byte[], int) and
        // update(byte[], int, int, byte[], int), we use already initialized obuffer.
        if (done) return -1;
        ofinish = 0;
        ostart = 0;
        int expectedOutputSize = cipher.getOutputSize(ibuffer.length);
        if (obuffer == null || expectedOutputSize > obuffer.length) {
            obuffer = new byte[expectedOutputSize];
        }
        int readin = input.read(ibuffer);
        if (readin == -1) {
            done = true;
            try {
                // doFinal resets the cipher and it is the final call that is made. If there isn't
                // any more byte available, it returns 0. In case of any exception is raised,
                // obuffer will get reset and therefore, it is equivalent to no bytes returned.
                ofinish = cipher.doFinal(obuffer, 0);
            } catch (IllegalBlockSizeException | BadPaddingException e) {
                obuffer = null;
                throw new IOException(e);
            } catch (ShortBufferException e) {
                obuffer = null;
                throw new IllegalStateException("ShortBufferException is not expected", e);
            }
        } else {
            // update returns number of bytes stored in obuffer.
            try {
                ofinish = cipher.update(ibuffer, 0, readin, obuffer, 0);
            } catch (IllegalStateException e) {
                obuffer = null;
                throw e;
            } catch (ShortBufferException e) {
                // Should not reset the value of ofinish as the cipher is still not invalidated.
                obuffer = null;
                throw new IllegalStateException("ShortBufferException is not expected", e);
            }
        }
        return ofinish;
    }

    /**
     * Constructs a CipherInputStream from an InputStream and a
     * Cipher.
     * <br>Note: if the specified input stream or cipher is
     * null, a NullPointerException may be thrown later when
     * they are used.
     * @param is the to-be-processed input stream
     * @param c an initialized Cipher object
     */
    public FasterCipherInputStream(InputStream is, Cipher c) {
        super(is);
        input = is;
        cipher = c;
    }

    /**
     * Constructs a CipherInputStream from an InputStream without
     * specifying a Cipher. This has the effect of constructing a
     * CipherInputStream using a NullCipher.
     * <br>Note: if the specified input stream is null, a
     * NullPointerException may be thrown later when it is used.
     * @param is the to-be-processed input stream
     */
    protected FasterCipherInputStream(InputStream is) {
        super(is);
        input = is;
        cipher = new NullCipher();
    }

    /**
     * Reads the next byte of data from this input stream. The value
     * byte is returned as an <code>int</code> in the range
     * <code>0</code> to <code>255</code>. If no byte is available
     * because the end of the stream has been reached, the value
     * <code>-1</code> is returned. This method blocks until input data
     * is available, the end of the stream is detected, or an exception
     * is thrown.
     * <p>
     *
     * @return  the next byte of data, or <code>-1</code> if the end of the
     *          stream is reached.
     * @exception  IOException  if an I/O error occurs.
     * @since JCE1.2
     */
    public int read() throws IOException {
        if (ostart >= ofinish) {
            // we loop for new data as the spec says we are blocking
            int i = 0;
            while (i == 0) i = getMoreData();
            if (i == -1) return -1;
        }
        return ((int) obuffer[ostart++] & 0xff);
    };

    /**
     * Reads up to <code>b.length</code> bytes of data from this input
     * stream into an array of bytes.
     * <p>
     * The <code>read</code> method of <code>InputStream</code> calls
     * the <code>read</code> method of three arguments with the arguments
     * <code>b</code>, <code>0</code>, and <code>b.length</code>.
     *
     * @param      b   the buffer into which the data is read.
     * @return     the total number of bytes read into the buffer, or
     *             <code>-1</code> is there is no more data because the end of
     *             the stream has been reached.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.InputStream#read(byte[], int, int)
     * @since      JCE1.2
     */
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    /**
     * Reads up to <code>len</code> bytes of data from this input stream
     * into an array of bytes. This method blocks until some input is
     * available. If the first argument is <code>null,</code> up to
     * <code>len</code> bytes are read and discarded.
     *
     * @param      b     the buffer into which the data is read.
     * @param      off   the start offset in the destination array
     *                   <code>buf</code>
     * @param      len   the maximum number of bytes read.
     * @return     the total number of bytes read into the buffer, or
     *             <code>-1</code> if there is no more data because the end of
     *             the stream has been reached.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.InputStream#read()
     * @since      JCE1.2
     */
    public int read(byte b[], int off, int len) throws IOException {
        if (ostart >= ofinish) {
            // we loop for new data as the spec says we are blocking
            int i = 0;
            while (i == 0) i = getMoreData();
            if (i == -1) return -1;
        }
        if (len <= 0) {
            return 0;
        }
        int available = ofinish - ostart;
        if (len < available) available = len;
        if (b != null) {
            System.arraycopy(obuffer, ostart, b, off, available);
        }
        ostart = ostart + available;
        return available;
    }

    /**
     * Skips <code>n</code> bytes of input from the bytes that can be read
     * from this input stream without blocking.
     *
     * <p>Fewer bytes than requested might be skipped.
     * The actual number of bytes skipped is equal to <code>n</code> or
     * the result of a call to
     * {@link #available() available},
     * whichever is smaller.
     * If <code>n</code> is less than zero, no bytes are skipped.
     *
     * <p>The actual number of bytes skipped is returned.
     *
     * @param      n the number of bytes to be skipped.
     * @return     the actual number of bytes skipped.
     * @exception  IOException  if an I/O error occurs.
     * @since JCE1.2
     */
    public long skip(long n) throws IOException {
        int available = ofinish - ostart;
        if (n > available) {
            n = available;
        }
        if (n < 0) {
            return 0;
        }
        ostart += n;
        return n;
    }

    /**
     * Returns the number of bytes that can be read from this input
     * stream without blocking. The <code>available</code> method of
     * <code>InputStream</code> returns <code>0</code>. This method
     * <B>should</B> be overridden by subclasses.
     *
     * @return     the number of bytes that can be read from this input stream
     *             without blocking.
     * @exception  IOException  if an I/O error occurs.
     * @since      JCE1.2
     */
    public int available() throws IOException {
        return (ofinish - ostart);
    }

    /**
     * Closes this input stream and releases any system resources
     * associated with the stream.
     * <p>
     * The <code>close</code> method of <code>CipherInputStream</code>
     * calls the <code>close</code> method of its underlying input
     * stream.
     *
     * @exception  IOException  if an I/O error occurs.
     * @since JCE1.2
     */
    public void close() throws IOException {
        if (closed) {
            return;
        }

        closed = true;
        input.close();

        // Android-removed: Removed a now-inaccurate comment
        if (!done) {
            try {
                cipher.doFinal();
            }
            catch (BadPaddingException | IllegalBlockSizeException ex) {
                // Android-changed: Added throw if bad tag is seen.  See b/31590622.
                if (ex instanceof AEADBadTagException) {
                    throw new IOException(ex);
                }
            }
        }
        ostart = 0;
        ofinish = 0;
    }

    /**
     * Tests if this input stream supports the <code>mark</code>
     * and <code>reset</code> methods, which it does not.
     *
     * @return  <code>false</code>, since this class does not support the
     *          <code>mark</code> and <code>reset</code> methods.
     * @see     java.io.InputStream#mark(int)
     * @see     java.io.InputStream#reset()
     * @since   JCE1.2
     */
    public boolean markSupported() {
        return false;
    }
}

It worked fine for my case while decrypting a file over 30 MB. Hope someone can find some flaws though worked really well for my case.

Edit: Sorry somehow I missed that the above answer says the same. Keeping it for others in case they just need to copy from somewhere. Thanks.

Rowdyism answered 9/6, 2021 at 22:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.