我试图使用密码类将任何java对象(在本例中是Integer,但日期也应该有效)加密到base64字符串。基本上,我使用ByteArrayOutputStream将给定的对象转换为字节数组,并用密码加密这个字节数组。见下文
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对象具有相同前缀的问题,而使用相同方法对String进行编码似乎很好。每个编码和加密的字符串都会导致另一个加密的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%。
发布于 2019-11-27 18:21:12
在加密之前使用对象序列化不是一个好主意。要么对数据进行加密以进行传输保护,在这种情况下,TLS更有意义。或者您要对较长时间的存储进行加密,在这种情况下,序列化是危险的,因为序列化格式可能会更改。见鬼,将来你可能想要改变整个语言/运行时。
我建议你制定自己的协议。例如,在这种情况下,您可以使用ByteBuffer#putInt(int)
或使用DataOutputStream#writeInt(int)
将整数编码为4个字节。这样,您的整数只需要最少4个字节(作为一个无符号的32位大endian值)。对于非常复杂的方法,您甚至可以查看ASN.1结构和编码(这些结构和编码是在Bouncy城堡和其他库中实现的)。
Java实际上只是内部的一个long
,它可以完美地存储在8个字节中。另一种选择是将其编码为(UTC)日期字符串,并使用US兼容编码(StandardCharsets.US_ASCII
)存储该字符串。
当心欧洲央行的模式是非常危险的。例如,假设0x00FFFFFF
之上的值是不常见的,并且您不想泄漏这些值的存在。另外,假设最重要的字节是块的最后一个字节,否则将填充标头字节。在这种情况下,很容易将使用0x01
的块与使用0x00
的块区分开来,这在这种情况下应该更常见。因此,您立即泄漏有关您的明文的信息。
在CBC模式下,如果使用静态IV而不是随机(或至少完全不可预测) IV值,则此问题同样突出。为了安全起见,必须使用每个CBC加密的随机IV。你可以把IV和密文一起存储。通常,对于CBC,16字节IV只是简单地以密文作为前缀。但是,最好是使用经过身份验证的GCM模式,使用12字节的随机零值模式。
Java完全允许重用密码实例,这有点遗憾--例如,它不允许Cipher
在使用后销毁关键材料。它默认为重复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
它包括nonce、整数的密文、表示日期的长值和"hello world“字符串,最后是身份验证标记,在Java中被认为是密文的一部分。
发布于 2019-11-27 19:37:49
您所看到的行为是使用非模式ECB
模式与明文中的相似性相结合的结果。对于所有块密码(AES、Blowfish、DES),您都会遇到同样的问题。
在使用CBC
时,如果您根据需要提供IVs,所有这些都会消失:
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。
发布于 2019-11-27 14:20:38
正如马克所指出的,ObjectOutputStream
创建了一个对象头,所以常用的前缀是因为和,因为您没有使用salt 和,而是使用相同的加密密钥。
这些弱点使得加密解决方案(即您的代码)容易受到纯密文攻击的攻击,尽管算法本身非常好。您刚刚以不安全的方式实现了它。
https://stackoverflow.com/questions/59072016
复制相似问题