将Base64字符串转换为十六进制字符串的问题

4

简述:我错过了哪个边缘情况,或者我的将Base64字符串转换为十六进制字符串的算法有错误吗?

最近,我决定尝试Matasano Crypto Challenges,但出于某种原因,我决定在不使用库进行Hex和Base64字符串之间转换的情况下尝试编写第一个挑战。

我已经成功将Hex转换为Base64,但是从输出中可以看出,在尝试将Base64转换回Hex时存在轻微异常(例如,比较Base64到Hex输出的最后四个值)。

十六进制转Base64:
应该输出:SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t 实际输出:SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t

Base64转十六进制:
应该输出:49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d
实际输出:49276d206b696c6c696e6720796e717220627261696e206c696b65206120706e69732e6e6f3573206c717328726f2e6d

我使用https://conv.darkbyte.ru/来检查我的一些值,假设该网站上的代码是正确的,那么似乎我的问题与从Base64获取Base10表示有关,而不是从Base10到Hex:

十进制等价值 我的输出: 73、39、109、32、107、105、108、108、105、110、103、32、121、110113、114、32、98、114、97、105、110、32、108、105、107、101、32、97、32、pnis. no5s lqs(ro.m) 网站的输出: 73、39、109、32、107、105、108、108、105、110、103、32、121、111117、114、32、98、114、97、105、110、32、108、105、107、101、32、97、32、poisonous、mushroom 看起来所有有错误的值都集中在40-60和100-120之间,但我不确定接下来该怎么做。我猜这里可能存在一些我没有处理的特殊情况,但我不确定是什么。
相关代码:
    private static final Character[] base64Order = new Character[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
        'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e',
        'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', };

    private static final Character[] hexOrder = new Character[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a',
        'b', 'c', 'd', 'e', 'f' };

public static String base64ToHex(String base64) throws Exception {
    if (base64.length() % 4 != 0 || base64.contains("[^a-zA-Z0-9\\+/]"))
        throw new Exception("InputNotBase64");
    else {
        int charValue = 0;
        int index = 0;
        String hex = "";
        BitSet bits = new BitSet();
        for (int i = 0; i < base64.length(); i++) {
            charValue = base64.charAt(i);
            // get actual value from ASCII table
            if (charValue > 64 && charValue < 91)
                charValue -= 65;
            if (charValue > 96 && charValue < 123)
                charValue -= 71;
            /// loop that adds to the BitSet reads right-to-left, so reverse
            // the bits and then shift
            charValue = Integer.reverse(charValue << 24) & 0xff;
            charValue >>= 2;
            // append binary values to the BitSet
            while (charValue != 0L) {
                if (charValue % 2 != 0) {
                    bits.set(index);
                }
                index++;
                charValue >>= 1;
            }
            // account for trailing 0s
            while (index % 6 != 0) {
                index++;
            }
        }
        // read 8-bit integer value for hex-value lookup
        String temp;
        int remainder;
        for (int i = 0; i < index; i++) {
            charValue = (charValue | (bits.get(i) ? 1 : 0));
            if ((i + 1) % 8 == 0) {
                temp = "";
                while (charValue != 0L) {
                    remainder = charValue % 16;
                    temp = hexOrder[remainder] + temp;
                    charValue /= 16;
                }
                hex += temp;
            }
            charValue <<= 1;
        }
        return hex;
    }
}
1个回答

1

你的代码中忘记处理以下字符:'0','1','2','3','4','5','6','7','8','9','+','/'。如果你替换以下代码

if (charValue > 64 && charValue < 91)
    charValue -= 65;
if (charValue > 96 && charValue < 123)
    charValue -= 71;

通过

charValue = getPositionInBase64(charValue);

where

public static int getPositionInBase64(int n)
{
    for (int p = 0; p < base64Order.length; p++)
    {
        if (n == base64Order[p])
        {
            return p;
        }
    }
    return -1;
}

所有工作都正常

此外,当您使用字符而不是神奇数字时,代码更易读

if (charValue >= 'A' && charValue <= 'Z')
    charValue -= 'A';
...

在这种情况下,找出问题更容易。
因为您提出了问题,我将展示可能改进计算速度的方法。
准备以下表格并初始化一次。
// index = character, value = index of character from base64Order
private static final int[] base64ToInt = new int[128];

public static void initBase64ToIntTable()
{
    for (int i = 0; i < base64Order.length; i++)
    {
        base64ToInt[base64Order[i]] = i;
    }
}

现在,您可以通过简单的操作替换if/else语句链。
charValue = base64ToInt[base64.charAt(i)];

使用这个方法,我写的速度比你快几倍。
private static String intToHex(int n)
{
    return String.valueOf(new char[] { hexOrder[n/16], hexOrder[n%16] });
}

public static String base64ToHexVer2(String base64) throws Exception
{
    StringBuilder hex = new StringBuilder(base64.length()*3/4); //capacity could be 3/4 of base64 string length
    if (base64.length() % 4 != 0 || base64.contains("[^a-zA-Z0-9\\+/]"))
    {
        throw new Exception("InputNotBase64");
    }
    else
    {
        for (int i = 0; i < base64.length(); i += 4)
        {
            int n0 = base64ToInt[base64.charAt(i)];
            int n1 = base64ToInt[base64.charAt(i+1)];
            int n2 = base64ToInt[base64.charAt(i+2)];
            int n3 = base64ToInt[base64.charAt(i+3)];
            // in descriptions I treat all 64 base chars as 6 bit
            // all 6 bites from 0 and 1st 2 from 1st (00000011 ........ ........)
            hex.append(intToHex(n0*4 + n1/16));
            // last 4 bites from 1st and first 4 from 2nd (........ 11112222 ........)
            hex.append(intToHex((n1%16)*16 + n2/4));
            // last 2 bites from 2nd and all from 3rd (........ ........ 22333333)
            hex.append(intToHex((n2%4)*64 + n3));
        }
    }
    return hex.toString();
}

我怀疑这段代码之所以更快,主要是因为将其转换为十六进制更简单。如果你想要并需要测试它,可以使用以下结构来测试速度。
    String b64 = "SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t";
    try
    {
        Base64ToHex.initBase64ToIntTable();
        System.out.println(Base64ToHex.base64ToHex(b64));
        System.out.println(Base64ToHex.base64ToHexVer2(b64));

        int howManyIterations = 100000;
        Date start, stop;
        long period;

        start = new Date();
        for (int i = 0; i < howManyIterations; i++)
        {
            Base64ToHex.base64ToHexVer2(b64);
        }
        stop = new Date();
        period = stop.getTime() - start.getTime();
        System.out.println("Ver2 taken " + period + " ms");

        start = new Date();
        for (int i = 0; i < howManyIterations; i++)
        {
            Base64ToHex.base64ToHex(b64);
        }
        stop = new Date();
        period = stop.getTime() - start.getTime();
        System.out.println("Ver1 taken " + period + " ms");

    }
    catch (Exception ex)
    {
    }

示例结果为

49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d
49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d
Ver2 taken 300 ms
Ver1 taken 2080 ms

但这只是一个近似值。当您首先检查Ver1,然后将Ver2作为第二个选项时,结果可能会略有不同。此外,使用不同的java版本(6、7、8)和不同的启动java设置可能导致结果不同。

谢谢,长时间盯着自己的代码后错过小细节是再正常不过了。你有其他替代方法获取Base64位置的建议吗?虽然这可能不会有太大的区别,但遍历数组似乎会增加不必要的时间。我考虑过使用字母而不是像你提到的“魔法”数字,但这会导致奇怪的事情,比如“if (value>= 'a' && value <= 'z') charValue -= 'G';”,这对清晰度没有什么帮助,而数字/两个符号将被非可打印ASCII字符移位... - Asmodean
我的代码能用吗?你的代码在与数字和符号相关的更正后是否能够工作?我认为两个答案都应该是肯定的 :). 如果您想提高性能,可以准备翻译表并使用char作为索引直接获取值。我可以在另一个答案中呈现它。根据小写字母的魔数,它应该是private static final int indexOfSmallA = 26; charValue -= 'a' - indexOfSmallAprivate static final int shiftOfSmallA = 71; charValue -= shiftOfSmallA。这只是建议,但可以帮助您在几年后快速修改代码 :)。 - marekzbrzozowa
我没有创建一个新答案。我编辑了这个。新年快乐。 - marekzbrzozowa

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