将字符数组转换为字节数组并再次转换为字符数组

43

我想将Java char数组转换为byte数组,而不创建中间的String,因为char数组包含密码。我查找了几种方法,但它们似乎都失败了:

char[] password = "password".toCharArray();

byte[] passwordBytes1 = new byte[password.length*2];
ByteBuffer.wrap(passwordBytes1).asCharBuffer().put(password);

byte[] passwordBytes2 = new byte[password.length*2];
for(int i=0; i<password.length; i++) {
    passwordBytes2[2*i] = (byte) ((password[i]&0xFF00)>>8); 
    passwordBytes2[2*i+1] = (byte) (password[i]&0x00FF); 
}

String passwordAsString = new String(password);
String passwordBytes1AsString = new String(passwordBytes1);
String passwordBytes2AsString = new String(passwordBytes2);

System.out.println(passwordAsString);
System.out.println(passwordBytes1AsString);
System.out.println(passwordBytes2AsString);
assertTrue(passwordAsString.equals(passwordBytes1) || passwordAsString.equals(passwordBytes2));
断言始终失败(并且在生产中使用代码时,密码被拒绝),但打印语句三次打印出密码。为什么 passwordBytes1AsString 和 passwordBytes2AsString 与 passwordAsString 不同,但看起来相同?我是否遗漏了空终止符或其他内容?我该如何使转换和反转换正常工作?

为什么您想避免创建中间字符串? - KarlP
14
Sun公司推荐这样做是最佳实践:http://download.oracle.com/javase/1.5.0/docs/guide/security/jce/JCERefGuide.html#PBEEx 字符串是不可变的,因此无法像字符数组一样清零 - 相反,您的密码会在内存中挂起一段不确定的时间。 - Scott
8个回答

17

将字符与字节之间进行转换是字符集编解码。我更喜欢在代码中尽可能地使其清晰明了。这并不意味着会增加额外的代码量:

 Charset latin1Charset = Charset.forName("ISO-8859-1"); 
 charBuffer = latin1Charset.decode(ByteBuffer.wrap(byteArray)); // also decode to String
 byteBuffer = latin1Charset.encode(charBuffer);                 // also decode from String

顺便说一下:

java.nio类和java.io读写器类使用ByteBuffer和CharBuffer(它们使用byte[]和char[]作为后备数组)。因此,如果直接使用这些类,则通常更好。但是,您始终可以执行以下操作:

 byteArray = ByteBuffer.array();  byteBuffer = ByteBuffer.wrap(byteArray);  
 byteBuffer.get(byteArray);       charBuffer.put(charArray);
 charArray = CharBuffer.array();  charBuffer = ByteBuffer.wrap(charArray);
 charBuffer.get(charArray);       charBuffer.put(charArray);

15

Original Answer

    public byte[] charsToBytes(char[] chars){
        Charset charset = Charset.forName("UTF-8");
        ByteBuffer byteBuffer = charset.encode(CharBuffer.wrap(chars));
        return Arrays.copyOf(byteBuffer.array(), byteBuffer.limit());
    }

    public char[] bytesToChars(byte[] bytes){
        Charset charset = Charset.forName("UTF-8");
        CharBuffer charBuffer = charset.decode(ByteBuffer.wrap(bytes));
        return Arrays.copyOf(charBuffer.array(), charBuffer.limit());    
    }

使用StandardCharsets进行编辑

public byte[] charsToBytes(char[] chars)
{
    final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(chars));
    return Arrays.copyOf(byteBuffer.array(), byteBuffer.limit());
}

public char[] bytesToChars(byte[] bytes)
{
    final CharBuffer charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes));
    return Arrays.copyOf(charBuffer.array(), charBuffer.limit());    
}

这里是一个关于StandardCharsets的JavaDoc页面。 请注意JavaDoc页面上的内容:

这些字符集在Java平台的每个实现中都保证可用。


1
ByteBuffer使用得很好。然而,如果没有另外说明,密码是Unicode,因此使用StandardCharset.UTF_8比将其减少到ASCII更好,以避免数据损坏。 - Tom Blodget
你可以使用任何你需要的字符集。 - Cassian
1
我已经编辑了这篇文章,将编码从US-ASCII改为UTF-8。你是对的。想法是保持相同的编码方式。US-ASCII没有像UTF-8那样多的字符,例如没有带重音的字母,如果你先使用UTF-8然后再使用US-ASCII,你会丢失一些信息。 - Cassian
3
把敏感数据存储在char[]或byte[]中后,需要按照Andrii在这里的使用说明清除敏感数据 https://dev59.com/T2035IYBdhLWcg3wSN8G#9670279 - Cassian
不错的例子。但在我的情况下,它与Charset charset = Charset.forName("ISO-8859-1");一起工作。 - RoutesMaps.com

12

问题出在使用了String(byte[])构造函数,这个构造函数使用平台默认编码,这几乎是你不应该做的 - 如果你将字符编码设置为“UTF-16”,你的测试可能会通过。目前我怀疑passwordBytes1AsStringpasswordBytes2AsString每个字符串长度为16个字符,其中每隔一个字符是U+0000。


我刚刚尝试了这个(即String passwordBytes1AsString = new String(passwordBytes1, "UTF-16");),但没有任何变化。我还尝试检查字符串的长度 - String.length() 返回8。它会计算U+0000字符吗? - Scott
@Scott:尝试打印字符串的长度和单个字符(作为int值)。这将显示出差异所在。 - Jon Skeet
112、97、115、115、119、111、114、100 的原始值和转换后的值。 - Scott
刚刚注意到我在断言中使用了错误的参数来调用equals()方法。*扇自己一个耳光*你最初的假设确实是正确的。非常感谢。 - Scott

5
我会使用循环将其转换为字节,再使用另一个循环将其转换回字符。
char[] chars = "password".toCharArray();
byte[] bytes = new byte[chars.length*2];
for(int i=0;i<chars.length;i++) {
   bytes[i*2] = (byte) (chars[i] >> 8);
   bytes[i*2+1] = (byte) chars[i];
}
char[] chars2 = new char[bytes.length/2];
for(int i=0;i<chars2.length;i++) 
   chars2[i] = (char) ((bytes[i*2] << 8) + (bytes[i*2+1] & 0xFF));
String password = new String(chars2);

4
如果想使用ByteBuffer和CharBuffer,请不要简单地使用.asCharBuffer()方法,该方法只会进行UTF-16(LE或BE,取决于您的系统 - 您可以使用order方法设置字节序)转换(因为Java字符串及其内部使用此编码的char[])。
应该使用Charset.forName(charsetName)方法,然后使用它的encode或decode方法,或newEncoder / newDecoder方法。
将byte[]转换为String时,还应指定编码方式(并且应该是相同的)。

3

这是对Peter Lawrey答案的扩展。为了使整个字符范围的反向(字节到字符)转换正确工作,代码应该如下:

char[] chars = new char[bytes.length/2];
for (int i = 0; i < chars.length; i++) {
   chars[i] = (char) (((bytes[i*2] & 0xff) << 8) + (bytes[i*2+1] & 0xff));
}

在使用前,我们需要将字节“取消签名”(& 0xff)。否则,可能无法正确返回所有可能的字符值的一半。例如,位于[0x80..0xff]范围内的字符将受到影响。

2

您应该使用getBytes()而不是toCharArray()

替换这行代码

char[] password = "password".toCharArray();

使用
byte[] password = "password".getBytes();

5
不要使用String#getBytes()而不指定编码,这会导致各种可移植性问题。 - eckes
这行代码并不适用于此用例:它只是在这个示例中获取char[]的简单方法。 - Cerber

1
当您在Java中使用GetBytes从字符串中获取字节时,返回结果将取决于计算机设置的默认编码(例如:StandardCharsetsUTF-8或StandardCharsets.ISO_8859_1等)。
因此,每当您想要从一个字符串对象中获取字节时,请确保提供一个编码。例如:
String sample = "abc";
Byte[] a_byte = sample .getBytes(StandardCharsets.UTF_8);

让我们检查一下代码发生了什么。 在Java中,命名为“sample”的字符串是由Unicode存储的。字符串中的每个字符都由2个字节存储。

sample :  value: "abc"   in Memory(Hex):  00 61 00 62 00 63
        a -> 00 61
        b -> 00 62
        c -> 00 63

但是,当我们从一个字符串中获取字节时,会有

标签。

Byte[] a_byte = sample .getBytes(StandardCharsets.UTF_8)
//result is : 61 62 63
//length: 3 bytes

Byte[] a_byte = sample .getBytes(StandardCharsets.UTF_16BE)  
//result is : 00 61 00 62 00 63        
//length: 6 bytes

为了获取字符串的原始字节,我们可以读取字符串的内存并获取每个字节。以下是示例代码:
public static byte[] charArray2ByteArray(char[] chars){
    int length = chars.length;
    byte[] result = new byte[length*2+2];
    int i = 0;
    for(int j = 0 ;j<chars.length;j++){
        result[i++] = (byte)( (chars[j] & 0xFF00) >> 8 );
        result[i++] = (byte)((chars[j] & 0x00FF)) ;
    }
    return result;
}

用法:

String sample = "abc";
//First get the chars of the String,each char has two bytes(Java).
Char[] sample_chars = sample.toCharArray();
//Get the bytes
byte[] result = charArray2ByteArray(sample_chars).

//Back to String.
//Make sure we use UTF_16BE. Because we read the memory of Unicode of  
//the String from Left to right. That's the same reading 
//sequece of  UTF-16BE.
String sample_back= new String(result , StandardCharsets.UTF_16BE);

问题没有提到 getBytes,所以这并不是很相关。你是想评论其他答案之一吗? - Simon MᶜKenzie
只是想声明一下关于String的getBytes函数的用法,以及在使用new String(Byte[])时需要注意什么。希望能有所帮助。 - junqiang chen

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