【问题标题】:AES-128 Encryption not working on Java < 1.7AES-128 加密不适用于 Java < 1.7
【发布时间】:2012-10-09 00:34:46
【问题描述】:

我已经花了 3 天的时间完成一项学校作业,今天终于完成了,没有错误并且工作正常!除了,我在 Java 1.7 上测试它,而学校服务器(教授将在其中编译它)运行 1.6。所以,我在 1.6 上测试了我的代码,想要覆盖我的所有基础,我在解密时得到一个BadPaddingException

[编辑] 警告:此代码不遵循常见的安全实践,不应在生产代码中使用。

最初,我有这个,它在 1.7 上运行良好(对不起,很多代码......所有相关......):

public static String aes128(String key, String data, final int direction) {
    SecureRandom rand = new SecureRandom(key.getBytes());
    byte[] randBytes = new byte[16];
    rand.nextBytes(randBytes);
    SecretKey encKey = new SecretKeySpec(randBytes, "AES");

    Cipher cipher = null;
    try {
        cipher = Cipher.getInstance("AES");
        cipher.init((direction == ENCRYPT ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE), encKey);
    } catch (InvalidKeyException e) {
        return null;
    } catch (NoSuchPaddingException e) {
        return null;
    } catch (NoSuchAlgorithmException e) {
        return null;
    }

    try {
        if (direction == ENCRYPT) {
            byte[] encVal = cipher.doFinal(data.getBytes());
            String encryptedValue = Base64.encode(encVal);
            return encryptedValue;
        } else {
            byte[] dataBytes = Base64.decode(data);
            byte[] encVal = cipher.doFinal(dataBytes);
            return new String(encVal);
        }
    } catch (NullPointerException e) {
        return null;
    } catch (BadPaddingException e) {
        return null;
    } catch (IllegalBlockSizeException e) {
        return null;
    }
}

但是,我的 BadPaddingException catch 块在解密时执行:

javax.crypto.BadPaddingException: Given final block not properly padded
        at com.sun.crypto.provider.SunJCE_f.b(DashoA13*..)
        at com.sun.crypto.provider.SunJCE_f.b(DashoA13*..)
        at com.sun.crypto.provider.AESCipher.engineDoFinal(DashoA13*..)
        at javax.crypto.Cipher.doFinal(DashoA13*..)
        at CipherUtils.aes128(CipherUtils.java:112)
        at CipherUtils.decryptFile(CipherUtils.java:44)
        at decryptFile.main(decryptFile.java:21)

这是我试图解决的问题(基本上,我自己添加了所有的填充/取消填充,并使用了NoPadding):

public static String aes128(String key, String data, final int direction) {
    // PADCHAR = (char)0x10 as String
    while (key.length() % 16 > 0)
        key = key + PADCHAR; // Added this loop

    SecureRandom rand = new SecureRandom(key.getBytes());
    byte[] randBytes = new byte[16];
    rand.nextBytes(randBytes);
    SecretKey encKey = new SecretKeySpec(randBytes, "AES");
    AlgorithmParameterSpec paramSpec = new IvParameterSpec(key.getBytes()); // Created this

    Cipher cipher = null;
    try {
        cipher = Cipher.getInstance("AES/CBC/NoPadding"); // Added CBC/NoPadding
        cipher.init((direction == ENCRYPT ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE), encKey, paramSpec); // Added paramSpec
    } catch (InvalidKeyException e) {
        return null;
    } catch (NoSuchPaddingException e) {
        return null;
    } catch (NoSuchAlgorithmException e) {
        return null;
    } catch (InvalidAlgorithmParameterException e) {
        return null; // Added this catch{}
    }

    try {
        if (direction == ENCRYPT) {
            while (data.length() % 16 > 0)
                data = data + PADCHAR; // Added this loop

            byte[] encVal = cipher.doFinal(data.getBytes());
            String encryptedValue = Base64.encode(encVal);
            return encryptedValue;
        } else {
            byte[] dataBytes = Base64.decode(data);
            byte[] encVal = cipher.doFinal(dataBytes);
            return new String(encVal);
        }
    } catch (NullPointerException e) {
        return null;
    } catch (BadPaddingException e) {
        return null;
    } catch (IllegalBlockSizeException e) {
        return null;
    }
}

当我使用这个时,我只是进进出出:

Out: u¢;èÉ÷JRLòB±J°N°[9cRÐ{ªv=]I¯¿©:
´RLA©êí;R([¶Ü9¸ßv&%®µ^#û|Bá (80)
Unpadded: u¢;èÉ÷JRLòB±J°N°[9cRÐ{ªv=]I¯¿©:
´RLA©êí;R([¶Ü9¸ßv&%®µ^#û|Bá (79)

另外值得注意的是,1.6 和 1.7 产生不同的加密字符串。

例如,在 1.7 上,使用密钥 hi 加密 xy(包括 SHA-1 哈希)会产生:

XLUVZBIJv1n/FV2MzaBK3FLPQRCQF2FY+ghyajdqCGsggAN4aac8bfwscrLaQT7BMHJgfnjJLn+/rwGv0UEW+dbRIMQkNAwkGeSjda3aEpk=

在 1.6 上,同样的事情会产生:

nqeahRnA0IuRn7HXUD1JnkhWB5uq/Ng+srUBYE3ycGHDC1QB6Xo7cPU6aEJxH7NKqe3kRN3rT/Ctl/OrhqVkyDDThbkY8LLP39ocC3oP/JE=

没想到作业要花这么长时间,所以我的时间已经不多了,确实需要今晚完成。但是,如果到那时还没有答案,我会就此给我的老师留个便条。这似乎是 1.7 中修复的一些问题......虽然希望可以通过我的代码中的正确添加/修复来解决。

非常感谢大家的时间!

【问题讨论】:

  • 嗯,值得注意的是,由于您的 encKey 是随机的,因此每次加密时您都应该期待不同的结果。但对我来说,它不适用于 Java7Java6(注意:我做了一些小的修改以使代码正常工作;并包括我在网上找到的随意粘贴的 Base64 方法)
  • @Eric 我不相信这行得通。我没有过多地使用 Java 加密,但乍一看,您似乎每次都使用不同的密钥(感谢SecureRandom),因此您不可能得到相同的结果。这也意味着你不可能让它正确解密,即使你解决了填充问题。
  • 作业描述对我来说意义不大。你能发布一个链接吗?拉伸密钥很好(就像@par 所描述的那样),但它必须是确定性的(即:它不能是随机的)所以你每次都得到相同的密钥。密码的随机部分是IV,它用于ECB 以外的操作模式。使用它是为了在加密相同的明文时不会得到相同的密文。这也必须与加密数据一起存储,以便用于解密。
  • 哦,顺便说一句,如果您需要使用已知种子的可重复随机数字,请使用 java.util.Random,而不是 SecureRandom。这将解决您的关键问题。
  • @TimBender 因为如果您在 ECB 以外的任何操作模式下使用分组密码(不要使用 ECB!),您需要一个初始化向量。该 IV,无论它是如何生成的,都必须存储在某个地方以进行解密。

标签: java cryptography aes


【解决方案1】:

首先:

对于几乎所有系统,两次加密相同的明文应该总是(即以非常高的概率)产生不同的密文。

传统的例子是它允许 CPA 对手仅用两个查询来区分 E(“黎明攻击”)和 E(“黄昏攻击”)。 (有少数系统需要确定性加密,但正确的方法是“合成 IV”或 CMC 和 EME 等密码模式。)

最终,问题在于SecureRandom() 不适合用于密钥派生。

  • 如果输入的“密钥”是密码,您应该使用类似 PBKDF2(或 scrypt()bcrypt())的东西。
    • 此外,您应该使用显式字符集,例如String.getBytes("UTF-8")
  • 如果输入的“key”是一个键,最常见的字符串表示是一个 hexdump。 Java 不包含 unhexing 函数,但有几个 here
    • 如果输入是“主密钥”并且您想要派生一个子密钥,那么您应该与其他数据一起对其进行散列。如果子键始终相同,那没有多大意义。

额外的挑剔:

  • 您的代码容易受到 padding oracle 攻击;您确实应该在对数据进行任何操作之前验证 MAC(或者更好的是,使用经过身份验证的加密模式)。
  • 在您的第二个清单中,您明确重复使用 IV。坏的!假设 CBC 模式,使用的 IV 应该是不可预测的; SecureRandom 在这里很有用。

【讨论】:

  • 虽然我很乐意接受这个作为答案,但建议的更改超出了我的任务范围。无论如何,我都给了你一个赞成票,并希望其他人也这样做,因为这是非常信息。谢谢!
  • @Eric:另外,如果您对加密背后的一些理论感兴趣,我发现 Coursera Cryptography course 非常好并且可以合理访问,但如果您已经很忙,可能会有点多学生。
  • @tc。谢谢!下学期的课程负担较轻时,我会研究一下。
  • Eric,如果它的结果​​是创建一个易受攻击的密文,这是一个奇怪的任务。
  • @owlstead:我还没有看到一个 CCA 安全的加密分配——它太容易出错了,我认为教人们他们正在编写“安全”加密最终是一种伤害(事件 MAC 验证可以有一个简单的定时侧信道)。教人们如何执行 padding oracle 攻击会更有趣(也更有效率)。
【解决方案2】:

我一遍又一遍地查看,我必须同意 NullUserException。问题在于SecureRandom 的使用。这意味着您永远不会真正知道您的密钥是什么,因此它不一定是同一个密钥。

encKey 来自 SecureRandom,它由提供的密钥播种。因此,如果key相同,则seed相同,所以random应该相同...

...当然,除非 Oracle(或其他提供商)更改版本之间的实现。

好的,添加更多我研究过的信息。我认为this answer was most helpful

从用户那里获取密码和明文,并将它们转换为字节数组。
生成一个安全的随机盐。
将盐附加到密码并计算其加密哈希。重复多次。
使用生成的哈希作为初始化向量和/或密钥来加密明文。
保存盐和生成的密文。

对我来说,听起来SecureRandom 曾被用于生成salt,但随后salt 必须与密码文本一起保存,以撤消密码过程。额外的安全性来自步骤的重复和变化(晦涩)。

注意:我没有一致认为这些步骤是最佳做法。

【讨论】:

  • 参见stackoverflow.com/questions/8259272/… -- SecureRandom 将在 Linux 上使用 /dev/urandom 并且是真正随机的。
  • re:最佳实践,问题显然是家庭作业,使用 PRNG 的密钥生成方法不是解决问题的好方法。您需要一种确定性的方法来获得预定数量的比特,并且用户倾向于提供低熵密码,例如“爱”或“上帝”或类似的密码。一个强大的密码短语可能会提供太多位。通常这就是为什么通过 SHA256 运行用户输入的任何内容都是获得可重复密钥位的好方法。这样您就不必依赖可以更改的 PRNG 实现,并且您会得到一个每次都有效的密钥。
  • 我会说使用盐是个好主意,但它必须在加密位之外传递,要么存储在某个地方,要么由算法使用可重复的 PRNG 生成,但你又来了存在确保 PRNG 实现不会从您的控制下更改的问题,它可以使用 SecureRandom。
  • 好吧,我已经使用 NullUserException 的信息以及 par 的 Random 想法完成了所有工作,其中大部分都包含在这个答案中,所以我接受了它。感谢你们三个!
猜你喜欢
  • 2019-04-18
  • 1970-01-01
  • 1970-01-01
  • 2014-01-31
  • 1970-01-01
  • 2013-04-09
  • 1970-01-01
  • 2020-12-03
  • 1970-01-01
相关资源
最近更新 更多