【问题标题】:AES encrypt any java object to a base64 stringAES 将任何 java 对象加密为 base64 字符串
【发布时间】:2020-03-23 02:48:09
【问题描述】:

我正在尝试使用 Cipher 类将任何 java 对象(在此示例中为整数,但 Date 也应该工作)加密为 base64 字符串。 基本上,我使用 ByteArrayOutputStream 将给定对象转换为字节数组,并使用 Cipher 加密此字节数组。见下文

for (Integer i = 0; i < 10; i++) {

    ByteArrayOutputStream bos = new ByteArrayOutputStream();

    ObjectOutput oos = new ObjectOutputStream(bos);
    oos.writeObject(i);
    oos.flush();

    byte[] data = bos.toByteArray();


    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec("&E(H+MbQeThWmZq4".getBytes("UTF-8"), "AES"));

    String base64output = Base64.getEncoder().encodeToString(cipher.doFinal(data));

    System.out.println(i + " - " + base64output);
}

输出

0 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa94LOaOdEXeZZm8qNoELOLdj
1 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa97aK6ELffW8n7vEkNAbC9RW
2 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa97mJ1m8lVtjwfGbHbMO2rxu
3 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa942rroZJbe2KN0/t8ukOkWd
4 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa97rbkvF4HLzuvGTm4JMJw+2
5 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa94zvSlIQe8RQI8t5/H74ShO
6 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa97tNLWZHmR0rNkDXZtVWA2Y
7 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa94lr84KZ6MnUsPOFyJIfDTB
8 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa97e6ihJ8SXmz9sy9XXwWeAz
9 - BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa97neBL2tLG2TXgCI/wDuyMo

因为相同的前缀BroJyDQdUDVYwq6tUdk9UcgIX8R7+B474UFw/HFx9lGpDjC0ilKxw8fYd1hFB54f8shrn/XIT52WzcOsH0CBGJ3bva8Rk1h4uYo5sfpJa9,我觉得很奇怪 对于每个加密对象。在此示例中,我对每个对象使用相同的键,但这不应该是此问题的原因。

我还使用字符串和日期而不是整数测试了这个示例。将日期编码为字节数组并使用相同的方法对其进行加密也会导致 进入这个问题,所有 Date 对象都有相同的前缀,而用相同的方法编码字符串似乎工作正常。每一个编码和加密 字符串导致另一个加密的 base64 字符串。见下文

加密日期的输出:(也具有相同的前缀)

0 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JRj1HrbSaioOqhbM2uZi2r0
1 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JQ0q0kophfAfiPxe0U+sb1R
2 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JTeTKnbYsLo6TjfuQF9PYIk
3 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JSrDPGtepg4HWUL6VeBtWg7
4 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JS7dlSsNjnY011F2BooNnKW
5 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JStO2xPQvT76/k+xMdaDBpQ
6 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JQqz4J3yO8G9taHi7b/Zefl
7 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JR8/fOAiuGM8tO8zMcju4Xk
8 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JSMDHi6UyD5QQY1jRXNCErc
9 - cpQxMKQW7mHCKsxxsyMMJTRPnfgujbJYLiVKeHgM2JRfKstfsC8dPYuPfd9f2B+B

加密字符串的输出:(按预期工作)

0 - TNpI3oLRzH5id6c/yRJlQQ==
1 - yMkm+ZuYWs4EnISo56Zljw==
2 - 03i1Lv01Nn2sGDGmtpRAIg==
3 - 5skvWbkcVXfT2TScaGxNfQ==
4 - 0p9qg5U+DqAnCBdyji+L9Q==
5 - gD5xPtAMy34xC90hKCQeWA==
6 - oQwKUhuxC5X/f6U9G9la8Q==
7 - 72cvCiLks3DDaTLAQvoVfw==
8 - wQu7Ug5RHg5egbNTI0YXQw==
9 - x1BQVwy3r6MP3SDLl/mktw==

有什么想法吗?

编辑: 即使我使用 CBC 或其他加密方法,如 DES 或 Blowfish,也会出现同样的问题。我希望 ByteArrayOutputStream 中的每个字节数组都应该加密成一个完全不同的 base64 字符串,即使它们具有相同的前缀,其长度约为 90%。

【问题讨论】:

  • 序列化的整数看起来非常长。尝试检查序列化整数的字节数组 (data)。我从来没有这样做过,但我怀疑他们有一个标题或类似的东西。
  • 我猜这是 ByteArrayOutputStream 创建的数据类型本身的标头信息。在我上次的编辑中,我已经提到这不应该是一个原因(在我看来)。
  • @ke_let 密码学与意见无关。你没有使用盐,所以你得到了通用前缀。加入盐,它们就消失了。
  • @ke_let 在尝试 CBC 时,您很可能省略了初始化向量 (IV),请参阅下面的答案。

标签: java arrays encryption aes encryption-symmetric


【解决方案1】:

在加密之前使用对象序列化不是一个好主意。要么加密数据以进行传输保护,在这种情况下 TLS 更有意义。或者您正在加密以进行更长时间的存储,在这种情况下,序列化是危险的,因为序列化格式可能会改变。哎呀,您将来可能想更改整个语言/运行时。

我建议您生成自己的协议。在这种情况下,您可以例如使用ByteBuffer#putInt(int) 或使用DataOutputStream#writeInt(int) 将整数编码为4 个字节。这样你的整数只需要最少的 4 个字节(作为一个无符号的 32 位大端值)。对于非常复杂的方法,您甚至可以查看 ASN.1 结构和编码(在 Bouncy Castle 以及其他库中实现)。

Java Date 内部实际上只是一个long,可以完美地存储在8 个字节中。另一种选择是将其编码为 (UTC) 日期字符串并使用美国 ASCII 兼容编码 (StandardCharsets.US_ASCII) 进行存储。


请注意,ECB 模式非常危险。例如,假设0x00FFFFFF 以上的值并不常见,并且您不想泄露这些值的存在。还可以想象,最重要的字节是块的最后一个字节,否则会用头字节填充。在这种情况下,很容易将块与例如0x01 来自带有0x00 的块,在这种情况下应该更常见。所以你立即泄露你的明文信息。

这个问题在 CBC 模式中同样突出如果您使用静态 IV 而不是随机(或至少完全不可预测的)IV 值。您必须为每个 CBC 加密使用随机 IV 以确保安全。您可以将 IV 与密文一起存储。通常对于 CBC,16 字节的 IV 只是简单地作为密文的前缀。但是,您最好使用经过身份验证的 GCM 模式和 12 字节随机随机数。

有点遗憾的是,Java 允许重用 Cipher 实例根本 - 因为它不允许 Cipher 在使用后销毁密钥材料。它默认为重复 IV 的不安全模式是双重可耻的。您必须自己处理 IV 问题。


使用 GCM 和ByteBuffer 的示例:

public static void main(String[] args) throws Exception {

    // input, a date and message
    Date date = new Date();
    String message = "hello world";

    // AES-128 key (replace by a real 256 bit key in your case)
    SecretKey aesKey = new SecretKeySpec(new byte[128 / Byte.SIZE], "AES");

    // default nonce sizes for GCM, using a constant should be preferred
    int nonceSize = 96;
    int tagSize = 128;

    String cts;
    try (StringWriter stringWriter = new StringWriter(); PrintWriter out = new PrintWriter(stringWriter)) {

        for (Integer i = 0; i < 10; i++) {
            byte[] randomNonce = createRandomIV(nonceSize);
            GCMParameterSpec gcmSpec = new GCMParameterSpec(128, randomNonce);

            byte[] encodedMessage = message.getBytes(StandardCharsets.UTF_8);

            ByteBuffer encodedNumberDateAndMessage = ByteBuffer.allocate(Integer.BYTES + Long.BYTES + encodedMessage.length);

            encodedNumberDateAndMessage.putInt(i);
            encodedNumberDateAndMessage.putLong(date.getTime());
            encodedNumberDateAndMessage.put(encodedMessage);
            // for reading we need to flip the buffer
            encodedNumberDateAndMessage.flip();

            ByteBuffer encryptedNumberDateAndMessage =
                    ByteBuffer.allocate(nonceSize / Byte.SIZE + encodedNumberDateAndMessage.limit() + tagSize / Byte.SIZE);

            encryptedNumberDateAndMessage.put(randomNonce);

            Cipher gcm = Cipher.getInstance("AES/GCM/NoPadding");
            gcm.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec);

            gcm.doFinal(encodedNumberDateAndMessage, encryptedNumberDateAndMessage);
            // not required, we'll be using array() method
            // encryptedNumberDateAndMessage.flip();

            // we can use the full array as there 
            String base64Ciphertext = Base64.getEncoder().encodeToString(encryptedNumberDateAndMessage.array());
            if (i != 0) {
                out.write('\n');
            }
            out.write(base64Ciphertext);
        }
        cts = stringWriter.toString();
    }
    System.out.println(cts);

    // TODO decrypt ciphertexts in cts
    // hint use BufferedReader to read lines and don't forget to strip off the IV/Nonce first
}

private static byte[] createRandomIV(int sizeInBits) {
    if (sizeInBits % Byte.SIZE != 0) {
        throw new IllegalArgumentException("Invalid IV size, must be a multiple of 8 bits");
    }
    byte[] randomNonce = new byte[sizeInBits / Byte.SIZE];
    SecureRandom rbg = new SecureRandom();
    rbg.nextBytes(randomNonce);
    return randomNonce;
}

这会产生看似随机的输出:

LHMsZPgZOz7nEcN5adB03+twTG2/ITfPnUUy4DxdgFEBAxm3HNDg8eXVnuvo80i4WMjY
eRJuw1ynrD3GeMmFTYiQc6VxelJuz8wHZtbl+7cepteKdtzcsdIDcDHBqvfjyzZp6WXd
MOkTLt4pk+sFm6I+CH4c90lxrRmwFKmS1wbX5eRSZYy6xqEjSz6iGC1vBXkPbl3k1C5r
cB5hKbpiAeNmbZYy1vdK5vissWYlkL6h6XJEYEFZaK7M097LkVAB01nu5GtCBUjPMjrK
LHzr/iudU3BPYmrimAIugjSckzXrzm03Ucgyb8laKktbh/Um4K2nyAGE2+T1aLH6JaYX
dg9SmcPl+dolHSIQPyvMUEPyu3VLSNPbN7ErPY93sjfKVyZsaGgft/cP4kUzNWEyRgAo
PiLHu4TKZMfBlFXst1867hEywST3RBbSSQ1g9D4DOkqh3oPkvsXP5INIEANZr2BHta38
4pJITAvij26NphYf9/ry5yGm+qPAaNG0Hqrk5ruVa60+V7k0jqDozjsST8OygyvkLrgY
HI6I3UHgzBNjskSJeo9fS3Cw3oKY8tneFbChtLz35DbcASOjpi7U9LKTL39lBTOBaZkG
jRycn4uSfT6JlDk3jn64wTL07I7bHvTSPSbWVG7XdKeSgOibW7FiCtTXojDPi8iywD58

由随机数、整数密文、表示日期的长值和“hello world”字符串以及最后的身份验证标记组成,在 Java 中被认为是密文的一部分。

【讨论】:

  • 感谢你的好榜样!我正在尝试修改它,以便我可以将它一般用于任何 Object 类型。你能告诉我nonce/nonceSize和tagSize是什么意思吗?并且 GCMParameterSpec(128, randomNonce) 是否也应该被 GCMParameterSpec(256, randomNonce) 替换以使用 265 位密钥?我也猜想,您将 Byte.SIZE 划分为独立于关于这些数据类型的不同系统实现?
  • 对于 any (可序列化)对象,您可能必须执行序列化,因为我当然不能为所有可能的对象定义二进制编码,但您至少应该知道所涉及的危险并提供开发特定的测量,以使您的序列化对象保持可解码。 GCMParameterSpec 的第一个参数是authentication tag(MAC)的大小,应该保持为128——它受块大小的限制;它不是密钥大小。最后Byte.SIZE 只是用来避免无法解释的“魔法值”8 - 过分热心?也许。
  • 如果你选择序列化,上面的代码当然也是安全的,但是它需要一些重写来生成编码的明文消息。不过,您已经在代码中这样做了。如果使用 AES-128 或 AES-256 仅取决于密钥本身的大小,否则 Cipher 算法的所有属性都保持不变。
【解决方案2】:

您看到的行为是您使用 no-mode ECB 模式与明文中的相似性相结合的结果。所有分组密码(AES、Blowfish、DES)都会遇到同样的问题。

当使用 CBC 时,如果您根据需要提供 IV,那么一切都会消失

public class Main {
    static Random rand = new SecureRandom();

public static IvParameterSpec generateIv() {
    byte[] ivBytes = new byte[16];
    rand.nextBytes(ivBytes);
    return new IvParameterSpec(ivBytes);
}

public static void main(String[] args) throws Exception {

    for (Integer i = 0; i < 10; i++) {

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutput oos = new ObjectOutputStream(bos);
        oos.writeObject(i);
        oos.flush();

        byte[] data = bos.toByteArray();

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = generateIv();
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec("&E(H+MbQeThWmZq4".getBytes("UTF-8"), "AES"), iv);

        String base64output = Base64.getEncoder().encodeToString(cipher.doFinal(data));
        System.out.println(i + " - " + base64output);
    }
}
}

输出:

0 - jt3Mk13pGjeaFf1oNq4LfmQ4z/31nRG4KtZ4H3RK6k/GA1anC3/lrzSXoLsQ6jMsVEpnxU13wAu6lkZJ3it1Ei4i4EsNFixc+YX4K6cIIv4ByY5Q246jd3H0m11C2FZJ
1 - Jqd0RB6lOITqifAaWluW6jx8F8gY4btZHx12CiXtZjfnehhtk64jva4eGTQd4EpvB/5Q/ORhZCNgF3ue0/Na1R9MCsK+mULAcyANdNcLyKbXo272G21z0LPCeweXdjhu
2 - xHdCG6rWNDyLTl8zruo8u+45V/RMXkrB7K+QU5r9lpc3FzvDwpl0wmy9Yj3FOyjMulmVT1zahH+wWVrmB9gNcXy7sGyCH/anJANC396OcDyQXqNIyvOPw9mpUmmRQcwR
3 - ygIDkLtQTupkbB35SzRflE3RAMmdYGSkdGZgRctFHdZCqGt+Arb3RbvhoAiiE9PwkyLmifyllQTTSutvV/ZtlGaGMX3v4bQUZDoaSyXQd9xn+pUSJk87NDVGi37xWw1O
4 - cJYSthCHHGeCqnuBJY8YdUbptKD3XNb2nt+pyIc94vRvjquYf7atu0+bDndFnePWvrlPzFIFXVB8CuANIsDhzRSNEOOU/wOkwcAN2AdavCqlZqN0Mtqdg4vqKGWx2oAE
5 - f7/gu8fJ8jkyhRAXJkLqdnJMLjCfFSjq8ovjhlNcuDPk8N/mYlA2845PGgi74Kb/zCG1WH8NtFK06xrpn15KyUxSANxoQ6C9QnzE9sc4aZj5rUatWeekvBfbqngq3JpG
6 - PitP2MuX4/Yysso8dCl1h2VK3MKoU2YpyzvLgZ3hZX/cBzSWp9O0Eafzj6GIMvAGVaL0x0V+K2Wv4eBOLIhDczhJXvHmKvTU7ZJnAwI37JXkOecN4HJdAXfFqg2WkT5f
7 - 1Mj8WnSqgLE08qfeYC1a3nZQ1jszxbT9J+ClUy8rCYusZHiArQcCgCwrNbWbI2yVfRjYOpsuTgyq31fnuHrkVfGu6RhiRhucR0a0Dign5fSU71STKksweHQ+oYQJibnQ
8 - TgGDGlOFWyfKO50xxPTPOmSpEsmpIVtWfnXkhhAoRsbZwo6z4oAuBJQs8EibsOr/r8KY5UHRbp+q3SlDhBE3mWszybMdOVRQKyJ1lZVXpmxmjXp/W2AqitsjCTKQaHi+
9 - 4xUnNjT8P0WiPtYg6ojrrQZnF0gU0wnndNQdLfPOMxoDvWjfe5OuEcY55yDRIosdpkeItTMVN1CRL4WecFgM8mBIVlnssE4Q1GM87PWNHipGZ91+MJwdsr0yUfCsJyRv

顺便说一句,您使用的是 16 字节密钥并获得 AES-128 不是 AES-256。

【讨论】:

  • 您没有展示如何处理 IV,并且使用 Random 而不是 SecureRandom 进行任何与密码学相关的事情是一个非常大的危险信号。除此之外,整数在序列化后仍然占用太多字节,而且它们不是必需的。
  • 我的是一个玩具示例,至于输出的大小,这是意料之中的,因为 AES 块大小、PKCS5 填充和 base64 编码使输出增加了大约 30%。
  • 我的示例带有额外的日期和“hello message”,但字节数更少,包括 IV 和 AES-GCM 的身份验证标签。序列化将整数扩展了很多......
  • 太棒了!我只是在回答最初的问题。
  • 您能否在答案中至少将Random 替换为SecureRandom?我正在与 SO 上的不安全代码作斗争,但我仍然输了。不幸的是,对于加密代码,它运行并不意味着它是好的。
【解决方案3】:

正如 Mark 指出的那样,ObjectOutputStream 创建了一个对象头,所以通用前缀是因为那个 and 因为你没有使用盐 and 你'重新使用相同的加密密钥。

这些弱点使加密解决方案(即您的代码)容易受到ciphertext-only attacks 的影响,即使算法本身非常好。您刚刚以不安全的方式实现了它。

【讨论】:

  • 它仍然提出了一个问题,为什么仅仅一个 Integer 有这么大的标题,而更复杂的 Date 没有。
  • @MarkJeronimus 好吧,OP 可能把它们弄混了。我会说Integer 在序列化方面更复杂。 java.util.Date 内部充满了statictransient 字段,而实际的序列化只涉及一个long 值。 Integer 也是一个包含很多本机依赖项的包装器,例如需要考虑到 IntegerCache
  • 使用相同的密钥不应该'导致几乎相同的加密输出。我已经使用多个具有相同密钥和不同整数的在线加密器进行了尝试,这导致了完全不同的输出。
  • 好吧,如果我使用盐,盐本身是否应该对每个对象都是随机的?我需要保存盐以便以后解密吗?
  • 在线您可能只是在加密一个数字(int),而在您的示例中,您正在加密一个(自动装箱的)Integer 对象。 Java 的 Integer 没有自定义(反)序列化 afaik,因此它默认序列化整个对象,具有完全限定的类名和 serialVersionUID 等等。
猜你喜欢
  • 1970-01-01
  • 2013-09-19
  • 2011-12-08
  • 2016-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-04
相关资源
最近更新 更多