Thursday, January 2, 2014

Convert signed-byte to unsigned-byte

Java中的byte类型是有符号的(-128 - 127),而C#中的byte是无符号的(0 - 255)。

在单纯使用Java或者C#的时候都不会有问题。但是如果一个程序,分为多个部分,其中涉及到Java与C#进行交互(或者是通过socket传输,或者是读取另外一种语言输出的文件等等),那么就可能会产生问题了。

比如说在C#中,对一个文件进行签名,并将签名的结果,以数字的形式输出到了文件中:
static void Main(string[] args)
{
    X509Certificate2 x509 = new X509Certificate2(@"d:\keystore\personal.p12", "********");
    RSACryptoServiceProvider privateKey = (RSACryptoServiceProvider)x509.PrivateKey;

    string message = File.ReadAllText("d:/message.txt", Encoding.UTF8);
    byte[] messageData = Encoding.UTF8.GetBytes(message);

    byte[] signature = privateKey.SignData(messageData, CryptoConfig.MapNameToOID("SHA1"));

    string signatureStr = string.Join<byte>(";", signature);
    using (StreamWriter sw = new StreamWriter("d:/message.sign", false, Encoding.UTF8))
    {
        sw.WriteLine(signatureStr);
    }
    Console.WriteLine(signatureStr);
    Console.ReadKey();
}
在message.sign文件中保存了如下内容:
39;56;101;161;49;121;247;199;187;136;209;3;231;32;103;129;47;6;171;49;100;112;39;133;228;5;123;76;241;226;238;153;34;62;229;174;127;130;72;186;129;234;162;240;98;53;64;251;115;107;160;46;8;2;164;89;250;208;116;46;112;69;11;98;137;243;158;32;109;233;152;231;246;218;35;178;72;143;123;202;210;125;169;94;20;148;67;37;14;138;230;226;225;248;74;12;74;83;240;209;131;226;165;109;1;43;112;225;248;246;51;37;50;189;184;204;39;194;176;19;122;80;142;8;251;198;109;75
那么,此时使用Java来验证签名的时候,需要调用Signature 类的verify方法,方法签名如下:
public final boolean verify(byte[] signature) throws SignatureException
可以看到,这里需要的是一个byte数组,然而Java中的byte数组是有符号的,所以我们需要将上面签名的结果转换成signed-byte array。

Unsigned-byte 转换 Signed-byte
首先我们先要将上面的每个数字存放在int变量中,原因很简单,无法直接存入byte变量,因为已经超出了signed-byte的范围了;
使用Java的强制转换,将int转换成byte,转换的结果如下:
39;56;101;-95;49;121;-9;-57;-69;-120;-47;3;-25;32;103;-127;47;6;-85;49;100;112;39;-123;-28;5;123;76;-15;-30;-18;-103;34;62;-27;-82;127;-126;72;-70;-127;-22;-94;-16;98;53;64;-5;115;107;-96;46;8;2;-92;89;-6;-48;116;46;112;69;11;98;-119;-13;-98;32;109;-23;-104;-25;-10;-38;35;-78;72;-113;123;-54;-46;125;-87;94;20;-108;67;37;14;-118;-26;-30;-31;-8;74;12;74;83;-16;-47;-125;-30;-91;109;1;43;112;-31;-8;-10;51;37;50;-67;-72;-52;39;-62;-80;19;122;80;-114;8;-5;-58;109;75
我们都知道,数据从int转换成byte的时候会丢失精度,那么上面的这个转换在Java内部是怎么进行的呢?让我们以161为例,说明一下这个过程:
  1. int在内存中占用4 bytes,那么161就是这样的:00000000 00000000 00000000 10100001
  2. byte在内存中占1 byte,因此在转换的过程中上面的4 bytes的前三个字节会被截断,变成:10100001
  3. Java中byte是有符号的,因此,第一位的1表示负数。
  4. 由于计算机内部使用的数字都是用补码表示的,因此,想要得到我们可以看懂的数值,需要将补码转换为原码,得到结果 11011111b = -95。
  5. 当我们输出这个值的时候,就会得到 -95,这个值也就是161对应的signed-byte的值。
这里很奇特不是吗?Signed-byte和Unsigned-byte的对应关系,和我们想象的不一样,最起码和我的思维不一致。我一直认为对应关系是这样的:
128 -- -0(-128)
129 -- -1
130 -- -2

255 -- -127

但实际上却是这样的:
128 -- -0(-128)
129 -- -127
130 -- -126

255 -- -1

至于为什么需要这样对应,我是没有能力解释的。可能是其中包含了一些数学原理,如果有了解的朋友,麻烦帮忙解释一下。

继续把上述验证签名的代码补充完整:
public static void main(String[] args) {
    try {
        Signature signatureEngine = Signature.getInstance("SHA1WithRSA");

        KeyStore ks = KeyStore.getInstance("pkcs12");
        ks.load(new FileInputStream("d:/keystore/personal.p12"), "**********".toCharArray());
        Certificate cert = ks.getCertificate("mykey");

        signatureEngine.initVerify(cert);
        signatureEngine.update(Files.readAllBytes(Paths.get(URI.create("file:///d:/message.txt"))));

        byte[] signature = readSignature(Paths.get(URI.create("file:///d:/message.sign")));

        boolean verified = signatureEngine.verify(signature);
        System.out.println(verified ? "Successful" : "Failed");
    } catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException | InvalidKeyException | SignatureException ex) {
        throw new RuntimeException(ex);
    }
}

private static byte[] readSignature(Path signaturePath) {
    try {
        String signatureStr = Files
                .readAllLines(signaturePath, Charset.forName("UTF-8"))
                .get(0).trim();
        String[] signatureArr = signatureStr.split(";");
        byte[] signature = new byte[signatureArr.length];
        for (int i = 0; i < signatureArr.length; i++) {
            signature[i] = (byte) Integer.parseInt(signatureArr[i]);
        }
        return signature;
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

Signed-byte 转换Unsigned-byte
可想而知,这里Java的自动转换是帮不了我们的。因为byte本身是有符号的,而且int的精度要比byte大,所以如果直接将byte转换成Int,并不会丢失精度,得到的只是一个精度更大的有符号的int值。比如byte -126 转换成int后会变成int -126,并不会得到上面对应关系中的130。那么想要吧Signed-byte转换成Unsigned应该怎么做呢?首先,我们要明确一点,不可能转换为Unsigned-byte,因为Java不提供这种类型。我们能做到的仅仅是将Signed-byte转换为对应的Unsigned-byte的int表现形式。想要达到这个目的,我们只需要用Signed-byte & 0xFF即可实现。非常奇妙,同样,看看内部是怎么运算的,同样使用-95这个例子:
  1. 0xFF是一个int值,当一个byte和int进行运算的时候,byte会首先被转换成int
  2. -95 对应的二进制是10000000 00000000 00000000 01011111
  3. 计算机内部使用补码来表示,数字int -95对应的补码是11111111 11111111 11111111 10100001
  4. 0xFF对应的机器内部表示是 00000000 00000000 00000000 11111111
  5. 这样进行“与”运算后就会得到这样的二进制代码 00000000 00000000 00000000 10100001
  6. 因为是正数,所以补码和源码一致,无需进行转换,得到10进制表示161

同样,里面的数学原理我无法解释,请知道的朋友帮忙解释下。代码如下:
public static void main(String[] args) {
    byte[] data = {39, 56, 101, -95, 49, 121, -9, -57, -69, -120, -47, 3, -25, 32, 103, -127, 47, 6, -85, 49, 100, 112, 39, -123, -28, 5, 123, 76, -15, -30, -18, -103, 34, 62, -27, -82, 127, -126, 72, -70, -127, -22, -94, -16, 98, 53, 64, -5, 115, 107, -96, 46, 8, 2, -92, 89, -6, -48, 116, 46, 112, 69, 11, 98, -119, -13, -98, 32, 109, -23, -104, -25, -10, -38, 35, -78, 72, -113, 123, -54, -46, 125, -87, 94, 20, -108, 67, 37, 14, -118, -26, -30, -31, -8, 74, 12, 74, 83, -16, -47, -125, -30, -91, 109, 1, 43, 112, -31, -8, -10, 51, 37, 50, -67, -72, -52, 39, -62, -80, 19, 122, 80, -114, 8, -5, -58, 109, 75};
    int[] unsignedData = new int[data.length];
    for (int i = 0; i < data.length; i++) {
        unsignedData[i] = data[i] & 0xFF;
    }
    System.out.println(Arrays.toString(unsignedData));
}
执行上面的代码,得到如下输出:
[39, 56, 101, 161, 49, 121, 247, 199, 187, 136, 209, 3, 231, 32, 103, 129, 47, 6, 171, 49, 100, 112, 39, 133, 228, 5, 123, 76, 241, 226, 238, 153, 34, 62, 229, 174, 127, 130, 72, 186, 129, 234, 162, 240, 98, 53, 64, 251, 115, 107, 160, 46, 8, 2, 164, 89, 250, 208, 116, 46, 112, 69, 11, 98, 137, 243, 158, 32, 109, 233, 152, 231, 246, 218, 35, 178, 72, 143, 123, 202, 210, 125, 169, 94, 20, 148, 67, 37, 14, 138, 230, 226, 225, 248, 74, 12, 74, 83, 240, 209, 131, 226, 165, 109, 1, 43, 112, 225, 248, 246, 51, 37, 50, 189, 184, 204, 39, 194, 176, 19, 122, 80, 142, 8, 251, 198, 109, 75]
所有的signed-byte,被转换成unsigned的形式,通过比对可以发现和原来用C#生成签名是一样的,证明转换没有问题。

总结
  • Unsigned-byte 转换 Signed-byte:直接强制转换即可(int -> byte)
  • Signed-byte 转换 Unsigned-byte:将Signed-byte & 0xFF得到Unsigned-byte

如果不想深究原理,直接记住上面的结论也没有问题。

No comments :

Post a Comment