Winzipaes decrypts 10 MB file slowly on Android
I tried to decrypt a 10MB file from a zip file using AES encryption on the Samsung S5, but it is so slow and it really surprises me. I'm familiar with AES, so I don't know how long it takes. Below are the results of my tests. Can anyone tell me if these results are reasonable or not?
Is there a way to speed up AES decryption?
PS. I am using SpongyCastle to avoid a conflict with the bootloader and I have also modified winzipaes to use SpongyCastle.
Test 1
Device: Samsung S5
Mail archive: 7za a -tzip -mx = 0 -p1234 -mem = AES256 test.zip 1MB_file 10MB_file
1MB_file: 1 MB
10MB_file: 10 MBs
test.zip: 12.5 MBs
compression ratio: 1.00
Decryption and decompress:
-> 1MB_file: 3167 ms
-> 10MB_file: 34137 ms
Test 2
Device: Samsung S5
Mail archive: 7za a -tzip -mx = 1 -p1234 -mem = AES256 test.zip 1MB_file 10MB_file
1MB_file: 1 MB
10MB_file: 10 MBs
test.zip: 5.4 MBs
compression ratio: 2.31
Decryption and decompression:
-> 1MB_file: 1290 ms
-> 10MB_file: 15369 ms
Test 3
Device: Samsung S5
Mail archive: 7za a -tzip -mx = 9 -p1234 -mem = AES256 test.zip 1MB_file 10MB_file
1MB_file: 1 MB
10MB_file: 10 MBs
test.zip: 5.1 MBs
compression ratio: 2.46
Decryption and decompression:
-> 1MB_file: 1202 ms
-> 10MB_file: 14460 ms
winzipaes: https://code.google.com/p/winzipaes/
SpongyCastle: http://rtyley.github.io/spongycastle/
=============================================== === ================================
Using @Maarten Bodewes - owlstead solution
Test 2
Device: Samsung S5
Mail archive: 7za a -tzip -mx = 1 -p1234 -mem = AES256 test.zip 1MB_file 10MB_file
1MB_file: 1 MB
10MB_file: 10 MBs
test.zip: 5.4 MBs
compression ratio: 2.31
Decryption and decompression:
-> 1MB_file: 206 ms (was 1290 ms)
-> 10MB_file: 1782 ms (was 15369 ms)
source to share
Yes, there are ways to speed this up, since the original winzipaes uses a rather inefficient decryption method: it decrypts each block by calculating the IV and initializing the cipher (to decrypt the CTR mode). This could mean that the key is being reinitialized too often. In addition, processing data in blocks of 16 bytes is also not very efficient. This is mainly because the AES-CTR performed by WinZip uses a small endian counter instead of a large endian counter (as is standardized).
The decryption also appears to involve calculating the HMAC-SHA1 from the ciphertext, which will add significant overhead as well. If only the confidentiality of the stored text is required, you can skip this step, although MAC has significant security advantages, providing cryptographically secure integrity as well as authenticity.
To show what I mean, I created a small sample code that at least runs much faster on my Java SE machine. According to Wayne (original poster) this speeds up Android code by a factor of 10 or so, in my Java SE benchmarks I "only" see speedups about 3 times the original.
Changes:
- created a special counter mode used for ZIP
- simplified / optimized decrypter code from above
- remote double key (D'oh!) for each file
- do not check MAC (on relatively small returns, SHA1 is pretty fast)
- using
AESFastEngine
, never mind, but hey ...
It is very likely that the same optimizations can be made for encryption.
Notes:
-
.zip
the encryption refers to a single stored file and is therefore rather inefficient, since the key derivation must be performed once for the saved file as well. Encrypting the file itself.zip
will be much more efficient. - using the JCA version of the decrypter can provide acceleration, and also Android can use the OpenSSL code in later versions (it will have to do block cipher blocking though).
-
/**
* Adapter for bouncy castle crypto implementation (decryption).
*
* @author olaf@merkert.de
* @author owlstead
*/
public class AESDecrypterOwlstead extends AESCryptoBase implements AESDecrypter {
private final boolean verify;
public AESDecrypterOwlstead(boolean verify) {
this.verify = verify;
}
// TODO consider keySize (but: we probably need to adapt the key size for the zip file as well)
public void init( String pwStr, int keySize, byte[] salt, byte[] pwVerification ) throws ZipException {
byte[] pwBytes = pwStr.getBytes();
super.saltBytes = salt;
PBEParametersGenerator generator = new PKCS5S2ParametersGenerator();
generator.init( pwBytes, salt, ITERATION_COUNT );
cipherParameters = generator.generateDerivedParameters(KEY_SIZE_BIT*2 + 16);
byte[] keyBytes = ((KeyParameter)cipherParameters).getKey();
this.cryptoKeyBytes = new byte[ KEY_SIZE_BYTE ];
System.arraycopy( keyBytes, 0, cryptoKeyBytes, 0, KEY_SIZE_BYTE );
this.authenticationCodeBytes = new byte[ KEY_SIZE_BYTE ];
System.arraycopy( keyBytes, KEY_SIZE_BYTE, authenticationCodeBytes, 0, KEY_SIZE_BYTE );
// based on SALT + PASSWORD (password is probably correct)
this.pwVerificationBytes = new byte[ 2 ];
System.arraycopy( keyBytes, KEY_SIZE_BYTE*2, this.pwVerificationBytes, 0, 2 );
if( !ByteArrayHelper.isEqual( this.pwVerificationBytes, pwVerification ) ) {
throw new ZipException("wrong password - " + ByteArrayHelper.toString(this.pwVerificationBytes) + "/ " + ByteArrayHelper.toString(pwVerification));
}
cipherParameters = new KeyParameter(cryptoKeyBytes);
// checksum added to the end of the encrypted data, update on each encryption call
if (this.verify) {
this.mac = new HMac( new SHA1Digest() );
this.mac.init( new KeyParameter(authenticationCodeBytes) );
}
this.aesCipher = new SICZIPBlockCipher(new AESFastEngine());
this.blockSize = aesCipher.getBlockSize();
// incremented on each 16 byte block and used as encryption NONCE (ivBytes)
// warning: non-CTR; little endian IV and starting with 1 instead of 0
nonce = 1;
byte[] ivBytes = ByteArrayHelper.toByteArray( nonce, 16 );
ParametersWithIV ivParams = new ParametersWithIV(cipherParameters, ivBytes);
aesCipher.init( false, ivParams );
}
// --------------------------------------------------------------------------
protected CipherParameters cipherParameters;
protected SICZIPBlockCipher aesCipher;
protected HMac mac;
@Override
public void decrypt(byte[] in, int length) {
if (verify) {
mac.update(in, 0, length);
}
aesCipher.processBytes(in, 0, length, in, 0);
}
public byte[] getFinalAuthentication() {
if (!verify) {
return null;
}
byte[] macBytes = new byte[ mac.getMacSize() ];
mac.doFinal( macBytes, 0 );
byte[] macBytes10 = new byte[10];
System.arraycopy( macBytes, 0, macBytes10, 0, 10 );
return macBytes10;
}
}
Of course, you also need a link SICZIPCipher
:
/**
* Implements the Segmented Integer Counter (SIC) mode on top of a simple
* block cipher. This mode is also known as CTR mode. This CTR mode
* was altered to comply with the ZIP little endian counter and
* different starting point.
*/
public class SICZIPBlockCipher
extends StreamBlockCipher
implements SkippingStreamCipher
{
private final BlockCipher cipher;
private final int blockSize;
private byte[] IV;
private byte[] counter;
private byte[] counterOut;
private int byteCount;
/**
* Basic constructor.
*
* @param c the block cipher to be used.
*/
public SICZIPBlockCipher(BlockCipher c)
{
super(c);
this.cipher = c;
this.blockSize = cipher.getBlockSize();
this.IV = new byte[blockSize];
this.counter = new byte[blockSize];
this.counterOut = new byte[blockSize];
this.byteCount = 0;
}
public void init(
boolean forEncryption, //ignored by this CTR mode
CipherParameters params)
throws IllegalArgumentException
{
if (params instanceof ParametersWithIV)
{
ParametersWithIV ivParam = (ParametersWithIV)params;
byte[] iv = ivParam.getIV();
System.arraycopy(iv, 0, IV, 0, IV.length);
// if null it an IV changed only.
if (ivParam.getParameters() != null)
{
cipher.init(true, ivParam.getParameters());
}
reset();
}
else
{
throw new IllegalArgumentException("SICZIP mode requires ParametersWithIV");
}
}
public String getAlgorithmName()
{
return cipher.getAlgorithmName() + "/SICZIP";
}
public int getBlockSize()
{
return cipher.getBlockSize();
}
public int processBlock(byte[] in, int inOff, byte[] out, int outOff)
throws DataLengthException, IllegalStateException
{
processBytes(in, inOff, blockSize, out, outOff);
return blockSize;
}
protected byte calculateByte(byte in)
throws DataLengthException, IllegalStateException
{
if (byteCount == 0)
{
cipher.processBlock(counter, 0, counterOut, 0);
return (byte)(counterOut[byteCount++] ^ in);
}
byte rv = (byte)(counterOut[byteCount++] ^ in);
if (byteCount == counter.length)
{
byteCount = 0;
incrementCounter();
}
return rv;
}
private void incrementCounter()
{
// increment counter by 1.
for (int i = 0; i < counter.length && ++counter[i] == 0; i++)
{
; // do nothing - pre-increment and test for 0 in counter does the job.
}
}
private void decrementCounter()
{
// TODO test - owlstead too lazy to test
if (counter[counter.length - 1] == 0)
{
boolean nonZero = false;
for (int i = 0; i < counter.length; i++)
{
if (counter[i] != 0)
{
nonZero = true;
}
}
if (!nonZero)
{
throw new IllegalStateException("attempt to reduce counter past zero.");
}
}
// decrement counter by 1.
for (int i = 0; i < counter.length && --counter[i] == -1; i++)
{
;
}
}
private void adjustCounter(long n)
{
if (n >= 0)
{
long numBlocks = (n + byteCount) / blockSize;
for (long i = 0; i != numBlocks; i++)
{
incrementCounter();
}
byteCount = (int)((n + byteCount) - (blockSize * numBlocks));
}
else
{
long numBlocks = (-n - byteCount) / blockSize;
for (long i = 0; i != numBlocks; i++)
{
decrementCounter();
}
int gap = (int)(byteCount + n + (blockSize * numBlocks));
if (gap >= 0)
{
byteCount = 0;
}
else
{
decrementCounter();
byteCount = blockSize + gap;
}
}
}
public void reset()
{
System.arraycopy(IV, 0, counter, 0, counter.length);
cipher.reset();
this.byteCount = 0;
}
public long skip(long numberOfBytes)
{
adjustCounter(numberOfBytes);
cipher.processBlock(counter, 0, counterOut, 0);
return numberOfBytes;
}
public long seekTo(long position)
{
reset();
return skip(position);
}
public long getPosition()
{
byte[] res = new byte[IV.length];
System.arraycopy(counter, 0, res, 0, res.length);
for (int i = 0; i < res.length; i++)
{
int v = (res[i] - IV[i]);
if (v < 0)
{
res[i + 1]--;
v += 256;
}
res[i] = (byte)v;
}
// TODO still broken - owlstead too lazy to fix for zip
return Pack.bigEndianToLong(res, res.length - 8) * blockSize + byteCount;
}
}
source to share