ZIP 文件的 AES 加密不正确的第二个 16 字节块

AES Encryption for ZIP files incorrect 2nd 16 byte block

我正在使用 Java 的 Cipher 实现实现 Zip AES 加密。 这是我的加密代码:

public final class AesEncoder implements Encoder {

    private final Cipher cipher;
    private final Mac mac;
    private final byte[] salt;
    private final byte[] derivedPasswordVerifier;

    // AesStrength is an Enum with AES strength constants like salt or mac length
    public static AesEncoder create(AesStrength strength, char[] password) throws Exception {
        KeySpec spec = new PBEKeySpec(password, salt, 1000, strength.getSize());
        SecretKey secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(spec);
        byte[] iv = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

        Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secretKey.getEncoded(), "AES"), new IvParameterSpec(iv));

        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(new SecretKeySpec(macKey, "HmacSHA1"));
        return new AesEncoder(cipher, mac, salt, derivedPasswordVerifier);
    }

    private static byte[] generateSalt(AesStrength strength) {
        SecureRandom random = new SecureRandom();
        byte[] buf = new byte[strength.getSaltLength()];
        random.nextBytes(buf);
        return buf;
    }

    @Override
    public void encrypt(byte[] buf, int offs, int len) {
        try {
            // buf is INPUT DATA WITH LENGTH 16 bytes (alwasy, because AES requires it)
            cipher.update(buf, offs, len, buf);
            mac.update(buf, offs, len);
        } catch(Exception e) {
            throw new Zip4jException(e);
        }
    }

    // ...

}

我想加密以下文本:

abcdefghijklmnopqrstuvwxyz

没有细节,我用两个长度为 16 字节的块调用了 encrypt() 方法两次:abcdefghijklmnopqrstuvwxyz。我有使用 AES 算法加密的正确 ZIP 文件。

我在 WinZipWinRar 等任何压缩器中打开此 ZIP 文件并打开加密文件。结果我得到以下文本:

abcdefghijklmnopÄÝB`CÙ˜Wi¯

如您所见,第一个块已正确加密,但第二个块未正确加密。

我已经调查过这个问题。我找到了一个名为 zip4j 的可行解决方案,并发现了两个不同之处:

首先: 这个库有自定义 AES 实现 AESEngine;我使用 jdk 实现;

第二个: 这个库从第一个字节开始增加 初始化向量

但是在jdk中使用的com.sun.crypto.provider.CounterMode,从末尾递增向量:

P.S. 如果我将初始向量设置为 iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }

这是来自 CounterMode 的代码:

private static void increment(byte[] b) {
    int n = b.length - 1;
    while ((n >= 0) && (++b[n] == 0)) {
        n--;
    }
}

问题。有人注意到它吗?如何解决?

备注:

  1. 我的代码 Encryption/Decryption 工作正常。它可以正确压缩和解压缩文件。问题是另一个压缩器如 WinZip 或 WinRar 不能正确解密这个文件。
  2. 我已尝试检查 jdk 中的整个实现并修改初始向量以使其在第二个块加密之前正好 { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 }。结果 - 第二个块是 正确加密.

我已经解决了这个问题。问题是 java 的 AES 实现与 ZIP 标准有点不同。

我使用 java 的 Cypher 在我的 zip4jvm 库中实现了 AES 加密 class:

class AesEncoder {
    public static AesEncoder create(AesStrength strength, char[] password) {
        try {
            byte[] salt = strength.generateSalt();
            byte[] key = AesEngine.createKey(password, salt, strength);

            Cipher cipher = AesEngine.createCipher(strength.createSecretKeyForCipher(key));
            Mac mac = AesEngine.createMac(strength.createSecretKeyForMac(key));
            byte[] passwordChecksum = strength.createPasswordChecksum(key);

            return new AesEncoder(cipher, mac, salt, passwordChecksum);
        } catch(Exception e) {
            throw new Zip4jvmException(e);
        }
    }
}

@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
enum AesStrength {
    S128(128),
    S192(192),
    S256(256);

    private final int size;

    public final int saltLength() {
        return size / 16;
    }

    private int macLength() {
        return size / 8;
    }

    private int keyLength() {
        return size / 8;
    }

    public SecretKeySpec createSecretKeyForCipher(byte[] key) {
        return new SecretKeySpec(key, 0, keyLength(), "AES");
    }

    public SecretKeySpec createSecretKeyForMac(byte[] key) {
        return new SecretKeySpec(key, keyLength(), macLength(), "HmacSHA1");
    }

    public byte[] createPasswordChecksum(byte[] key) {
        final int offs = keyLength() + macLength();
        return new byte[] { key[offs], key[offs + 1] };
    }

    public byte[] generateSalt() {
        SecureRandom random = new SecureRandom();
        byte[] buf = new byte[saltLength()];
        random.nextBytes(buf);
        return buf;
    }

}

class AesEngine {
    private static final int ITERATION_COUNT = 1000;

    public static byte[] createKey(char[] password, byte[] salt, AesStrength strength) throws NoSuchAlgorithmException, InvalidKeySpecException {
        int keyLength = strength.getSize() * 2 + 16;
        KeySpec keySpec = new PBEKeySpec(password, salt, ITERATION_COUNT, keyLength);
        return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(keySpec).getEncoded();
    }

    public static Cipher createCipher(SecretKeySpec secretKeySpec) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
        Cipher cipher = Cipher.getInstance("AES");
        // use custom AES implementation, so no worry for DECRYPT_MODE
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        return cipher;
    }

    public static Mac createMac(SecretKeySpec secretKeySpec) throws NoSuchAlgorithmException, InvalidKeyException {
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(secretKeySpec);
        return mac;
    }

}

您可以在 AesEncoder.java

找到更多信息