【问题标题】:Decryption much slower compared to encryption on Android与 Android 上的加密相比,解密要慢得多
【发布时间】:2017-07-09 04:32:22
【问题描述】:

我在 Kotlin 中使用“CBC”模式和“PKCS5Padding”填充实现了“AES”加密和解密。我注意到,在解密 cipherInputStream.read(buffer) 时,一次只读取 512 个字节,而不是 8192 字节的完整缓冲区大小。这是为什么?加密时使用整个缓冲区。

这些是我正在使用的常量,

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

解密代码

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)
}

加密代码

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)
}

【问题讨论】:

  • 使用input.available() 是个坏主意,因为它可能不会到达流的末尾,此时您会产生不完整的明文。只需保留else-block 的内容并删除if else
  • 更新了我的实现
  • 您没有显示您的加密代码。 AES 解密通常比加密慢一点。它也不清楚“慢得多”是什么意思。 "...解密cipherInputStream.read(buffer) 仅读取 512 字节...而不是完整的缓冲区大小...为什么?" - 设备资源受限,“块”并不罕见或“块”数据到磁盘扇区的大小。使用磁盘扇区可以有效地处理内存和文件流。 8192 字节还不错,但要避免大分配。它避免了内存压力和操作系统消息,如onLowMemory()
  • @jww 我也添加了加密代码。我尝试调试加密和解密过程,发现在加密时,read 是 8192,这是完整的缓冲区大小,但在解密时,read 是 512,尽管实际缓冲区大小是 8192。为什么会有这样的差异?

标签: android encryption cryptography kotlin


【解决方案1】:

最近我遇到了类似的问题。

问题出在 CipherInputStream 类的内部缓冲区,定义如下

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

显着提高解密速度的是将此缓冲区大小增加到 8192。所以我只是将粘贴的原始 CipherInputStream 类复制到我自己的类并修改了缓冲区大小。

有趣的是这个 ibuffer 字段上方的注释。

512 字节的大小是随机选择的 */

希望对你有帮助

【讨论】:

    【解决方案2】:

    我刚刚通过更改ibuffer 的长度大小来实现类。 (仅复制粘贴更改后的值)

    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;
        }
    }
    

    在解密超过 30 MB 的文件时,它适用于我的情况。希望有人能找到一些缺陷,虽然对我的情况非常有效。

    编辑:对不起,我错过了上面的答案也是一样的。保留给其他人,以防他们只需要从某个地方复制。谢谢。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-07-22
      • 2014-04-30
      • 2018-03-27
      • 1970-01-01
      • 2016-07-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-30
      相关资源
      最近更新 更多