在Java中对两个字符串进行XOR操作

56

如何在Java中对两个字符串执行位异或操作。


2
你需要进一步完善你的问题。你期望得到什么结果?能否提供一个例子? - ChrisJ
2
我对你想要实现的内容很感兴趣。也许是某种加密方式? :) - Daniel
是的。我想加密并获得另一个字符串。 - yasitha
你可以使用Java加密API http://download.oracle.com/javase/1.5.0/docs/guide/security/jce/JCERefGuide.html。 - Dead Programmer
7个回答

54
你想要类似这样的东西:
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import java.io.IOException;

public class StringXORer {

    public String encode(String s, String key) {
        return base64Encode(xorWithKey(s.getBytes(), key.getBytes()));
    }

    public String decode(String s, String key) {
        return new String(xorWithKey(base64Decode(s), key.getBytes()));
    }

    private byte[] xorWithKey(byte[] a, byte[] key) {
        byte[] out = new byte[a.length];
        for (int i = 0; i < a.length; i++) {
            out[i] = (byte) (a[i] ^ key[i%key.length]);
        }
        return out;
    }

    private byte[] base64Decode(String s) {
        try {
            BASE64Decoder d = new BASE64Decoder();
            return d.decodeBuffer(s);
        } catch (IOException e) {throw new RuntimeException(e);}
    }

    private String base64Encode(byte[] bytes) {
        BASE64Encoder enc = new BASE64Encoder();
        return enc.encode(bytes).replaceAll("\\s", "");

    }
}

进行Base64编码是因为对一个字符串的字节进行异或操作可能不会得到有效的字节。


6
很好的答案!但读者应确保使用java.util.Base64,而不是即将无法访问的sun.misc - Nicolai Parlog
2
我使用了Android.Base64替代sun的示例代码:import android.util.Base64; 同时这两个方法改为如下形式:private byte[] base64Decode(String s) { try { return Base64.decode(s,Base64.DEFAULT); } catch (IllegalArgumentException e) {throw new RuntimeException(e);} } private String base64Encode(byte[] bytes) { return Base64.encodeToString(bytes,Base64.DEFAULT).replaceAll("\\s", ""); } - JohnC

27
注:这仅适用于小字符,即0x8000以下的字符。这适用于所有ASCII字符。
我会对每个charAt()执行XOR操作,以创建一个新的字符串,例如:
String s, key;

StringBuilder sb = new StringBuilder();
for(int i = 0; i < s.length(); i++)
    sb.append((char)(s.charAt(i) ^ key.charAt(i % key.length())));
String result = sb.toString();

回应 @user467257 的评论:

如果你的输入/输出是utf-8,并且你对“a”和“æ”进行异或运算,你将得到一个无效的utf-8字符串,其中只包含一个字符(十进制135,一个连续字符)。

被异或的是char值而不是字节值,这会产生一个可以UTF-8编码的字符。

public static void main(String... args) throws UnsupportedEncodingException {
    char ch1 = 'a';
    char ch2 = 'æ';
    char ch3 = (char) (ch1 ^ ch2);
    System.out.println((int) ch3 + " UTF-8 encoded is " + Arrays.toString(String.valueOf(ch3).getBytes("UTF-8")));
}

打印

135 UTF-8 encoded is [-62, -121]

4
首先,生成的字符串未经适当异或处理,这意味着您不能通过再次使用密钥进行异或来恢复原始字符串(除非您的密钥保证与消息相等或更长,这是非常奇怪的),这使得代码完全误解了异或操作的概念。其次,仅通过异或字符不能保证获得有效的字符串字节,因此输出字符串可能包含无效的字节序列。 - user467257
1
@user467257,我认为你混淆了charbyte这两个不同的概念。我已经更新了我的回答并针对你的评论进行了回复。 - Peter Lawrey
1
我删除了我的两个评论,因为其中有太多的不准确之处。我认为“插入”额外字节的有效发生是在转换为char时,因为char将指向具有两个字节utf-8表示的代码点)。虽然我认为我可以想出一个更好的char wise xoring失败的例子,但我会在周末考虑一下。 - user467257
用户467257的担忧实际上是正确的,只是他使用了UTF-8的反例,而Java String使用UTF-16。对于UTF-16,我们只需要找到两个字符进行异或运算以产生代理项。例如:'\u11b0' ^ '\uc810'。如果您在具有不成对代理项的字符串上使用getBytes,它将为UTF-8生成?,并为UTF-16生成REPLACEMENT CHARACTER \ufffd - nhahtdh
2
@PeterLawrey 当你按照你的答案所提议的逐个字符进行异或运算时,只有在这种情况下才会存在限制。这是一种欺骗性的解决方案,容易陷入陷阱。更好的方法是逐字节进行异或运算,将结果进行Base64(或其他)编码以确保可打印/可读性,然后按相反的步骤进行解码。 - user467257
显示剩余8条评论

18

注意:

char 是 Java 中对应UTF-16编码单元的数据类型,有时候需要两个连续的char(即所谓的代理对)表示一个真正的Unicode字符(代码点)。

对于两个有效的UTF-16序列(比如Java字符串逐个char进行XOR运算或将其编码为UTF-16后逐个字节进行XOR运算),结果不一定是另一个有效的UTF-16字符串 - 结果可能会包含未配对的代理项(surrogates)。 (它仍然可以作为完全可用的Java字符串使用,只是与代码点相关的方法可能会混淆,并且将其转换为其他编码以进行输出等方法也可能会产生问题。)

如果您首先将字符串转换为UTF-8,然后再进行XOR运算,则您很可能会得到一个无效的UTF-8字节序列,如果您的字符串不是纯ASCII字符串,则会出现此情况。

即使您尝试正确地迭代两个字符串的代码点并尝试XOR这些代码点,您最终也可能得到超出有效范围的代码点(例如,U+FFFFF(第15平面)XOR U+10000(第16平面)=U+1FFFFF(可能是第31平面的最后一个字符),超出现有代码点的范围,您也可能以这种方式得到代理项保留的代码点(即无效代码点)。

如果字符串仅包含< 128, 256, 512, 1024, 2048, 4096, 8192, 16384, or 32768的字符,则按字符运算XOR的字符串将在相同的范围内,因此肯定不包含任何代理项。 在前两种情况下,您也可以分别将字符串编码为ASCII或Latin-1并获得相同的XOR字节结果。(但仍可能包含控制字符,这可能是个问题。)


我最终想说的是:不要期望加密字符串后的结果再次成为有效字符串——相反,只需将其存储和传输为byte[](或者字节流)。(是的,在加密之前转换为UTF-8,在解密之后从UTF-8转换回来)。


1
Java内部使用什么是无关紧要的。作为用户,您可以访问每个char(当然还涉及代理问题)或每个代码点。Java内部使用UTF-16还是月光宝盒小精灵穿着的颜色与问题无关。 - SyntaxT3rr0r
@SyntaxT3rr0r:好吧,可能措辞不太恰当,我正在尝试编辑。 - Paŭlo Ebermann
@SyntaxT3rr0r:通过代码点进行XOR操作也没有帮助(请参见现在答案中的示例)。 - Paŭlo Ebermann
1
+1 - 我同意保罗的观点。异或操作可能会破坏使Java字符串成为有效UTF-16字符串的属性。如果这样做,它们将变得无法编码/解码。 - Stephen C

4

这个解决方案适用于Android(我已经测试并使用过它)。感谢@user467257,我从他的解决方案中进行了适应。

import android.util.Base64;

public class StringXORer {

public String encode(String s, String key) {
    return new String(Base64.encode(xorWithKey(s.getBytes(), key.getBytes()), Base64.DEFAULT));
}

public String decode(String s, String key) {
    return new String(xorWithKey(base64Decode(s), key.getBytes()));
}

private byte[] xorWithKey(byte[] a, byte[] key) {
    byte[] out = new byte[a.length];
    for (int i = 0; i < a.length; i++) {
        out[i] = (byte) (a[i] ^ key[i%key.length]);
    }
    return out;
}

private byte[] base64Decode(String s) {
    return Base64.decode(s,Base64.DEFAULT);
}

private String base64Encode(byte[] bytes) {
    return new String(Base64.encode(bytes,Base64.DEFAULT));

}
}

谢谢!几个注意事项:base64Encode() 没有被使用过,最好使用 Base64.NO_WRAP 进行编码以避免出现换行符。 - gmk57

3

这是我正在使用的代码:

private static byte[] xor(final byte[] input, final byte[] secret) {
    final byte[] output = new byte[input.length];
    if (secret.length == 0) {
        throw new IllegalArgumentException("empty security key");
    }
    int spos = 0;
    for (int pos = 0; pos < input.length; ++pos) {
        output[pos] = (byte) (input[pos] ^ secret[spos]);
        ++spos;
        if (spos >= secret.length) {
            spos = 0;
        }
    }
    return output;
}

你好,能否请您解释一下,这个应该如何工作? - 5er
你好,请问你能给我解释一下这个程序应该如何工作吗?我的想法是这样的:
  1. 创建一个“秘密”
  2. 使用上述代码创建编码字符串并将其添加到源代码中。
  3. 在运行时解码此编码字符串。 每次使用相同的秘密和算法。 我的问题是,如何隐藏秘密,以便潜在的黑客无法获取我的公钥。
- 5er

3
假设字符串长度相等,为什么不将字符串转换为字节数组,然后对字节进行异或运算。由于编码方式的不同(如UTF8会因不同字符而扩展到不同的字节长度),得到的字节数组也可能具有不同的长度。
您应该注意指定字符编码以确保一致/可靠的字符串/字节转换。

2
字符串的长度可能相等,但字节数组的长度可能不同。 ;) - Peter Lawrey
@PeterLawrey,你能解释一下字节数组的长度何时会不同吗? - artaxerxe
1
如果你有"$".getBytes(),它可能是1个字节,"£"可能是2个字节,"€"可能是3个字节。(它们都是UTF-8编码) - Peter Lawrey
@PeterLawrey 这意味着任何具有大于255的int表示的char将在UTF-8中用多个字节表示吗? - artaxerxe
任何大于127的字符在UTF-8中使用超过一个字节。有些使用两个或三个字节。字符串可以包含代码点(大于65535的字符),它们可以使用4个字节。 - Peter Lawrey
1
请注意,Java中的代码点可以介于0(Character.MIN_CODE_POINT)和0x10FFFF(Character.MAX_CODE_POINT)之间。 - Peter Lawrey

2

abs函数用于在字符串长度不相同时,其结果长度将等于字符串a和b中较短的那一个。

public String xor(String a, String b){
    StringBuilder sb = new StringBuilder();
    for(int k=0; k < a.length(); k++)
       sb.append((a.charAt(k) ^ b.charAt(k + (Math.abs(a.length() - b.length()))))) ;
       return sb.toString();
}

你不需要在循环中计算绝对值。 - dieter

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