前面几篇博客简单归纳了Android中有涉及到的编码和加解密相关的问题,在这里想再补充下加密解密过程中遇到的字节数组和字符串的转换问题。前面博客中包含了转换的代码,由于这个属于所有加解密共性的问题,所以没有在每篇博客里去详细介绍。所以这篇博客就算是整理归纳下这个转换的问题吧。

1、为什么需要相互转换?

在加密时,一般加密算法和hash算法,它们操作的都是字节数组,对字节数组按照加密算法进行各种变换,运算,得到的结果也是字节数组。而我们一般是要求对字符串进行加密,所以就涉及到字符串String到 byte[] 的转换。
同时在解密时,需要的到解密后的字符串进行保存或对比,自然需要将字节数组byte[] 转换为 String 字符串。

2、一般String与byte[] 两者的转换

String→byte[]

这个转换简单通用,并且源码String API中有提供方法如下

/**
     * Encodes this {@code String} into a sequence of bytes using the given
     * {@linkplain java.nio.charset.Charset charset}, storing the result into 
     * a new byte array.
     * @param  charset
     *         The {@linkplain java.nio.charset.Charset} to be used to encode
     *         the {@code String}
     *
     * @return  The resultant byte array
     *
     * @since  1.6
     */
    public byte[] getBytes(Charset charset) {
        if (charset == null) {
            throw new NullPointerException("charset == null");
        }

        final String name = charset.name();
        if ("UTF-8".equals(name)) {
            return CharsetUtils.toUtf8Bytes(this, 0, count);
        } else if ("ISO-8859-1".equals(name)) {
            return CharsetUtils.toIsoLatin1Bytes(this, 0, count);
        } else if ("US-ASCII".equals(name)) {
            return CharsetUtils.toAsciiBytes(this, 0, count);
        } else if ("UTF-16BE".equals(name)) {
            return CharsetUtils.toBigEndianUtf16Bytes(this, 0, count);
        }

        return StringCoding.encode(charset, this);
    }

    /**
     * Encodes this {@code String} into a sequence of bytes using the
     * platform's default charset, storing the result into a new byte array.
     * @return  The resultant byte array
     *
     * @since      JDK1.1
     */
    public byte[] getBytes() {
        return getBytes(Charset.defaultCharset());
    }
byte[]→String

字节数组转化为字符串,虽然String也有提供相应的函数,但是要实现成功的转换而不出现乱码,不能简单直接的使用String函数,源码中String API 提供的函数如下:

/**
     * Constructs a new {@code String} by decoding the specified array of bytes
     * using the platform's default charset.  The length of the new {@code
     * String} is a function of the charset, and hence may not be equal to the
     * length of the byte array.
     *
     * <p> The behavior of this constructor when the given bytes are not valid
     * in the default charset is unspecified.  The {@link
     * java.nio.charset.CharsetDecoder} class should be used when more control
     * over the decoding process is required.*/

    public String(byte bytes[]) {
        this(bytes, 0, bytes.length);
    }

    /**
     * Constructs a new {@code String} by decoding the specified array of
     * bytes using the specified {@linkplain java.nio.charset.Charset charset}.
     * The length of the new {@code String} is a function of the charset, and
     * hence may not be equal to the length of the byte array.
     *
     * <p> This method always replaces malformed-input and unmappable-character
     * sequences with this charset's default replacement string.  The {@link
     * java.nio.charset.CharsetDecoder} class should be used when more control
     * over the decoding process is required.*/

    public String(byte bytes[], Charset charset) {
        this(bytes, 0, bytes.length, charset);
    }

3、byte[]→String出现乱码问题及原因分析

如果只是直接将加解密后得到字节数组使用 new String(byte)或者 new String(byte, charset)的方式转化为String字符串,得到的基本上都是乱码。
因为加密所采用的 MD5, SHA-256, SHA-512 等等算法,它们是通过对byte[] 进行各种变换和运算,得到加密之后的byte[],那么这个加密之后的 byte[] 结果显然 就不会符合任何一种的编码方案,比如 UTF-8, GBK等,因为加密的过程是任意对byte[]进行运算的。所以你用任何一种编码方案来解码 加密之后的 byte[] 结果,得到的都会是乱码。

4、加密后 byte[] 到String转换的方式一

加密解密时,采用的byte[] 到 String 转换的方法都是将 byte[] 二进制利用16进制的char[]来表示,每一个 byte 是8个bit,每4个bit对应一个16进制字符。所以一个 byte 对应于两个 16进制字符:

byte[]→String过程分析:
16进制表达方式是使用 0-9 abcdef 这16个数字和字母来表示 0-15 这16个数字的。所以我们在String转化时,可以用字符 ‘0’ 来表示 数字0, 可以用 ‘1’ 来表示 1,可以用 ‘f’ 来表示15。
HEX.charAt((b >> 4) & 0x0f) 这句是先右移四位得到高4位,然后再 & 0x0f 去除低4位,作为索引取得HEX字符串中对应的字符,通过这种方式,就可以讲 byte[] 数组转换成16进制字符表示的 char[]。最后通过将拼接后的StringBuilder.toString得到String类型的结果。所以最后的String是由:’0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’, ‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’ 这16个字符组成的String, 不含有任何其的字母。比如不会g,h,jklmn…..等等。

String →byte[]过程分析:
要将16进制编码的String转换成原来的byte[],第一步是将 String 类型转换到 char[] 数组,例如将 “012ab5ef” 转换成 [‘0’,’1’,’2’,’a’,’b’,’5’,’e’,’f’],然后将没两个相连的 char 转化成一个 byte. 显然 char[] 数组的大小必须是偶数的。其原理跟byte[]→String反向的过程。

/**
 * 提供加解密后 byte[]-->String 以及 String-->byte[]的转换工具类
 * Created by LGC on 2017/5/7.
 */

public class HexUtils {
    private final static String HEX = "0123456789ABCDEF";
    /**
     * 二进制转(16进制)字符串
     */
    private static String ByteToHex(byte[] bytesKey) {
        if (bytesKey == null)
            return "";
        StringBuilder sb = new StringBuilder();
        for (byte b : bytesKey) {
            sb.append(HEX.charAt((b >> 4) & 0x0f)).append(HEX.charAt(b & 0x0f));
        }
        return sb.toString();
    }

    /**
     * (16进制)字符转二进制
     */
    private static byte[] HexToByte(String hexString) {
        int len = hexString.length() / 2;
        byte[] result = new byte[len];
        for (int i = 0; i < len; i++)
            result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue();
        return result;
    }
}

5、加密后 byte[] 到String转换的方式二

利用Base64编码来实现转换

Base64.encodeToString("字节数组转化为字符串".getBytes(), Base64.DEFAULT);

Base64.decode("将字符串转化为字节数组",Base64.DEFAULT));

6、总结

首先,总结下两种转换方式的特点:
方式一:16进制编码的特点是全部由’0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’, ‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’ 这16个字符组成,不含其他字母。
方式二:Base64的编码其特点就是可能末尾有一个或者两个=,可能含有 / 和 + 字符。

其次,针对转换问题的应用总结:

由String 转换得到的 byte[] 就一定可以使用原来的编码方案转换成原来的 String,但是加密的结果 byte[] 却不能用任何字符编码方案得到String, 一般使用16进制编码或者利用Base64编码成String,然后进行存储或者比较。