草庐IT

java - winzipaes 在 Android 上解密 10 MB 的文件很慢

coder 2023-12-28 原文

我试图在 Samsung S5 上用 AES 加密从 zip 文件中解密一个 10 MB 的文件,但速度太慢了,这让我很吃惊。我对AES很熟悉,所以我不知道它是否消耗了很多时间。以下是我的测试结果。谁能告诉我这些结果是否合理?

有没有加速 AES 解密的方法?

附言。我使用 SpongyCaSTLe 来避免类加载器冲突,我还修改了 winzipaes 以使用 SpongyCaSTLe。

测试 1
设备:三星S5
压缩包:7za a -tzip -mx=0 -p1234 -mem=AES256 test.zip 1MB_file 10MB_file
1MB_文件:1 MB
10MB_文件:10 MB
测试.zip:12.5 MB
压缩率:1.00
解密解压:
--> 1MB_文件:3167 毫秒
--> 10MB_文件:34137 毫秒

测试 2
设备:三星S5
压缩包:7za a -tzip -mx=1 -p1234 -mem=AES256 test.zip 1MB_file 10MB_file
1MB_文件:1 MB
10MB_文件:10 MB
test.zip:5.4 MB
压缩率:2.31
解密解压:
--> 1MB_文件:1290 毫秒
--> 10MB_文件:15369 毫秒

测试 3
设备:三星S5
压缩包:7za a -tzip -mx=9 -p1234 -mem=AES256 test.zip 1MB_file 10MB_file
1MB_文件:1 MB
10MB_文件:10 MB
test.zip:5.1 MB
压缩率:2.46
解密解压:
--> 1MB_文件:1202 毫秒
--> 10MB_文件:14460 毫秒

winzipaes:https://code.google.com/p/winzipaes/
海绵城堡:http://rtyley.github.io/spongycastle/

============================================= ===================================

使用@Maarten Bodewes - owlstead 解决方案

测试 2
设备:三星S5
压缩包:7za a -tzip -mx=1 -p1234 -mem=AES256 test.zip 1MB_file 10MB_file
1MB_文件:1 MB
10MB_文件:10 MB
test.zip:5.4 MB
压缩率:2.31
解密解压:
--> 1MB_file:206 毫秒(原为 1290 毫秒)
--> 10MB_file:1782 毫秒(原为 15369 毫秒)

最佳答案

是的,有一些方法可以加快这个速度,因为 winzipaes 的源代码使用了一种相当低效的解密方式:它通过计算 IV 和初始化密码(用于 CTR 模式解密)来解密每个 block 。这可能意味着 key 重新初始化过于频繁。此外,以 16 字节为单位处理数据也不是很有效。这主要是因为 WinZip 执行的 AES-CTR 使用小端计数器而不是大端计数器(作为标准)。

解密似乎还包括在密文上计算 HMAC-SHA1,这也会增加大量开销。如果您只需要存储文本的 secret 性,那么您可以跳过该步骤,尽管 MAC 确实具有显着的安全优势,提供加密安全完整性和真实性。

为了说明我的意思,我创建了一个小示例代码,它至少在我的 Java SE 机器上运行得更快。根据 Wayne(原始发布者)的说法,这将 Android 代码的速度提高了 10 倍左右,在我的 Java SE 测试中,我“只”看到了大约 3 倍于原始代码的速度。

变化:

  • 创建了用于 ZIP 的特殊小端计数器模式
  • 由于上述原因简化/优化解密器代码
  • 删除了每个文件的双 key 派生(D'oh!)
  • 选择不验证 MAC(返回相对较小,SHA1 非常快)
  • 使用AESFastEngine,没关系,但是嘿...

很可能可以为加密器创建相同类型的优化。

注意事项:

  • .zip 加密是每个存储的文件,因此效率相当低,因为 key 派生也需要对每个存储的文件进行一次。 .zip 文件本身的加密会更有效率。
  • 使用 JCA 版本的解密器可能会提速,而且 Android 可能会在更高版本中使用 OpenSSL 代码(尽管它必须逐 block 加密)。

-

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

当然你还需要引用的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's 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;
    }
}

关于java - winzipaes 在 Android 上解密 10 MB 的文件很慢,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27703709/

有关java - winzipaes 在 Android 上解密 10 MB 的文件很慢的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  8. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  9. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐