AES密钥编码为byte[],再转换为字符串,然后再转回byte[]。

4
在类似问题“将byte[]转换为String,然后再转回byte[]”中说不要进行byte[]到String和再转回的转换,这看起来适用于大多数情况,尤其是当您不知道所使用的编码时。

但在我的情况下,我正在尝试保存javax.crypto.SecretKey数据到数据库,并在之后恢复它。

该接口提供了一个getEncoded()方法,返回编码为byte[]的密钥数据,使用另一个类,我可以使用这个byte[]来恢复密钥。

因此,问题是,如何将密钥字节写入字符串,然后获取byte[]以重新生成密钥?

4个回答

7

javax.crypto.SecretKey是二进制数据,因此不能直接转换为字符串。您可以将其编码为十六进制字符串或Base64格式。

请参见Apache Commons Codec

更新:如果您不想依赖第三方库(并且不能/不想存储纯二进制数据,如Jon所建议的),则可以进行一些临时编码,例如遵循erickson的建议:

public static String bytesToString(byte[] b) {
    byte[] b2 = new byte[b.length + 1];
    b2[0] = 1;
    System.arraycopy(b, 0, b2, 1, b.length);
    return new BigInteger(b2).toString(36);
}

public static byte[] stringToBytes(String s) {
    byte[] b2 = new BigInteger(s, 36).toByteArray();
    return Arrays.copyOfRange(b2, 1, b2.length);
}

这段代码相对来说比较丑陋,不符合标准,输出大小也不是最优的。但它非常小巧、正确,并且没有依赖关系;如果你的数据规模比较小,它还是很实用的。

更新:根据GregS的评论,我用字面值(36)替换了Character.MAX_RADIX。这样可能看起来不太优雅,但实际上更加安全。(你也可以使用32或16)。


一个好的解决方案,它甚至考虑了前导零。如果我可以挑剔一下,我会选择自己的常数,可能是32或16,而不是Character.MAX_RADIX。未来的java.lang.Character版本可能会使用比36更大的值。 - President James K. Polk
@GregS:你是对的。顺便说一下,Character.MAX_RADIX 目前是36。 - leonbloy
虽然答案是正确的且有效的,但我想知道:“SecretKey是二进制数据”是什么意思?对我来说,在某个层面上,所有东西都是二进制数据。你能否举个不是二进制数据的例子呢?谢谢! - The Student
@Tom:这取决于概念定义的级别。例如,当我们谈论HTML页面、JSON数据块、Java字符串、正则表达式中的模式、web.xml文件等等时,我们处理一些在文本世界中有意义的语法(人们也可以指定某种方式-编码-将其表示为二进制,但这将超出定义范围-并且通常可以自由地使用几个二进制编码)。 - leonbloy

3
使用Base-64编码可将任意二进制数据安全地转换为字符串,并进行反向转换。 Apache Commons Codec库提供了此功能的代码,其他一些库也有提供(尽管我不是特别喜欢Apache Commons Codec的API。 我目前没有了解到其他具有此功能的库,但它们肯定存在。)
编辑: 该项目提供了单个文件快速编码和解码,并且具有合理的方法签名,以及许多额外选项(如果需要)。
或者,如果目的是将其保存到数据库中,为什么不直接使用适当的二进制数据字段(Image / Blob或适用于您的数据库的其他内容),而不是在首次存储时将其作为字符串存储呢?

这是因为我对数据库还很陌生,我正在使用Derby并且硬编码SQL(将String作为参数)。用时间来研究数据库可能会更好。 :) - The Student
@Tom:我会尝试修复SQL而不是转换为字符串,除非Derby不支持二进制数据。如果你真的真的想坚持使用字符串,那么Base64肯定可以解决问题。 - Jon Skeet
Derby SQL语句仅支持字符串作为参数,我选择使用Apache Commons Code Hex类进行解码,因为我已经对它有一定的了解。 - The Student
我也是iharder解决方案的粉丝。 - President James K. Polk

1

将编码方式改为数字而不是字符字符串如何?虽然不如Base-64紧凑,但您可以省略Apache Commons。

/* Store the key. */
BigInteger n = new BigInteger(1, key.getEncoded()); /* Store as NUMBER, or … */
String s = n.toString(32);                          /* … store as VARCHAR. */

/* Reconstruct the key (may need to pad most significant bytes with zero). */
BigInteger n = new BigInteger(s); /* Load from VARCHAR, or … */
byte[] raw = n.toByte();          /* … load from NUMBER. */
byte[] original = new byte[16];
System.arraycopy(raw, 0, original, 16 - raw.length, raw.length);

明智的想法,在某些情况下是可行的(我曾使用过它)。我进行了一些更正(例如,我认为该代码不会在开头保留零字节),并添加到我的帖子中。 - leonbloy
它通过将值复制到目标数组的最低有效字节来恢复开始处的零字节。 - erickson

0

你不能使用String构造函数吗:String(byte[] data,String charsetName),就像这样:

byte[] data=new byte[1024];
String key=new String(data,"UTF-8");

然后你可以这样做:

String key="mymostsecretaeskey";
byte[] data=key.getBytes("UTF-8");

7
非常糟糕的想法——数据将是任意二进制数据,这并不总是一个有效的UTF-8序列。 - Jon Skeet
啊,好的,现在我明白问题了...是的,忘掉这个答案吧 :) - fasseg
但是当调用getEncoded()方法时,密钥不是使用系统默认字符集进行编码的吗?然后您只需调用key.getBytes()和new String(byte[] data)... - fasseg
@smeg4brains:你为什么这么认为?据我所知,秘密密钥根本不需要任何文本表示。 - Jon Skeet
2
@smeg4brains - 一些编码(例如US-ASCII)并不将每个值从0-255的范围映射到一个字符。当解码器发现域外的字节时,它会替换为“替换字符”(�)。当String再次编码为字节时,它们将不是原始字节。 - erickson
+1 因为没有你的帖子,我不会知道为什么这是一个坏主意.. :) - The Student

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接