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()
方法两次:abcdefghijklmnop
和 qrstuvwxyz
。我有使用 AES 算法加密的正确 ZIP 文件。
我在 WinZip
或 WinRar
等任何压缩器中打开此 ZIP 文件并打开加密文件。结果我得到以下文本:
abcdefghijklmnopÄÝB`CÙ˜Wi¯
如您所见,第一个块已正确加密,但第二个块未正确加密。
我已经调查过这个问题。我找到了一个名为 zip4j 的可行解决方案,并发现了两个不同之处:
首先: 这个库有自定义 AES 实现 AESEngine;我使用 jdk 实现;
第二个: 这个库从第一个字节开始增加 初始化向量 。
- 第一个区块 iv = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
- 第二块 iv = { 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
但是在jdk中使用的com.sun.crypto.provider.CounterMode,从末尾递增向量:
- 第一个区块 iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
- 第二块 iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
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--;
}
}
问题。有人注意到它吗?如何解决?
备注:
- 我的代码 Encryption/Decryption 工作正常。它可以正确压缩和解压缩文件。问题是另一个压缩器如 WinZip 或 WinRar 不能正确解密这个文件。
- 我已尝试检查 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
找到更多信息
我正在使用 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()
方法两次:abcdefghijklmnop
和 qrstuvwxyz
。我有使用 AES 算法加密的正确 ZIP 文件。
我在 WinZip
或 WinRar
等任何压缩器中打开此 ZIP 文件并打开加密文件。结果我得到以下文本:
abcdefghijklmnopÄÝB`CÙ˜Wi¯
如您所见,第一个块已正确加密,但第二个块未正确加密。
我已经调查过这个问题。我找到了一个名为 zip4j 的可行解决方案,并发现了两个不同之处:
首先: 这个库有自定义 AES 实现 AESEngine;我使用 jdk 实现;
第二个: 这个库从第一个字节开始增加 初始化向量 。
- 第一个区块 iv = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
- 第二块 iv = { 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
但是在jdk中使用的com.sun.crypto.provider.CounterMode,从末尾递增向量:
- 第一个区块 iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
- 第二块 iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
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--;
}
}
问题。有人注意到它吗?如何解决?
备注:
- 我的代码 Encryption/Decryption 工作正常。它可以正确压缩和解压缩文件。问题是另一个压缩器如 WinZip 或 WinRar 不能正确解密这个文件。
- 我已尝试检查 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
找到更多信息