如何检测编码不匹配
How to detect encoding mismatch
我有一堆旧的 AES 加密字符串,大致像这样加密:
- 字符串使用 ISO-8859-1 编码转换为字节
- 字节使用 AES 加密
- 结果转换为 BASE64 编码的字符数组
现在我想将新值的编码更改为 UTF8(例如,“€”不适用于 ISO-8859-1)。这意志的
如果我尝试使用 UTF-8 编码解密旧的 ISO-8859-1 编码值,当然会导致问题:
org.junit.ComparisonFailure: expected:<!#[¤%&/()=?^*ÄÖÖÅ_:;>½§@${[]}<|'äöå-.,+´¨]'-Lorem ipsum dolor ...> but was:<!#[�%&/()=?^*����_:;>��@${[]}<|'���-.,+��]'-Lorem ipsum dolor ...>
我正在考虑为此创建一些自动编码回退。
所以主要问题是是否足以检查解密的字符数组中的 '�' 字符以找出编码不匹配? 什么是 'correct' 比较时如何声明'�'符号?
if (new String(utf8decryptedCharArray).contains("�")) {
// Revert to doing the decrypting with ISO-8859-1
decryptAsISO...
}
解密时,你会得到原始的字节序列(你第一步的结果),然后你只能根据 ISO-8859-1 或 UTF-8 编码猜测这些字节表示字符。
从一个字节序列来看,没有办法清楚地说明它是如何被解释的。
一些想法:
- 您可以迁移所有旧的加密字符串(解密、使用 ISO-8859-1 解码为字符串、使用 UTF-8 编码为字节数组、加密)。那么问题一劳永逸地解决了。
- 你可以尝试解码两个版本的byte数组,看看一个版本是不是非法的,或者两个版本是否相等,如果还是不明确,就根据预期的字符取概率大的那个。我不建议这样做,因为它需要大量的工作,而且仍然有可能出错。
- 对于新条目,您可以在字符串/字节序列前添加一些未出现在 ISO-8859-1 文本中的标记。例如。有些人按照惯例在 UTF-8 编码文件的开头添加字节顺序标记。尽管生成的字节 (
EF BB BF
) 在 ISO-8859-1 中并非严格非法(读作 
),但它们极不可能。然后,当您解密的字节以 EF BB BF
开头时,使用 UTF-8 解码为字符串,否则使用 ISO-8859-1。尽管如此,还是有非零的错误概率。
如果可能的话,我会迁移现有条目。否则,您将不得不在您的代码库中永远使用“旧格式兼容性”,并且仍然不能绝对保证正确的行为。
将字节解码为文本时,不要依赖 �
字符来检测格式错误的输入。使用严格的解码器。这是一个辅助方法:
static String decodeStrict(byte[] bytes, Charset charset) throws CharacterCodingException {
return charset.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes))
.toString();
}
这里是对应的strict encoder helper方法,以备不时之需:
static byte[] encodeStrict(String str, Charset charset) throws CharacterCodingException {
ByteBuffer buf = charset.newEncoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.encode(CharBuffer.wrap(str));
byte[] bytes = buf.array();
if (bytes.length == buf.limit())
return bytes;
return Arrays.copyOfRange(bytes, 0, buf.limit());
}
由于 ISO-8859-1 允许所有字节,因此您不能使用它来检测格式错误的输入。然而,UTF-8 正在验证,因此它很可能检测到格式错误的输入。然而,这不是 100% 保证,但这是我们所能做到的最好的。
因此,尝试使用严格的 UTF-8 解码,如果失败则回退到 ISO-8859-1:
static String decode(byte[] bytes) {
try {
return decodeStrict(bytes, StandardCharsets.UTF_8);
} catch (CharacterCodingException e) {
return new String(bytes, StandardCharsets.ISO_8859_1);
}
}
测试
System.out.println(decode("señor".getBytes(StandardCharsets.ISO_8859_1))); // prints: señor
System.out.println(decode("señor".getBytes(StandardCharsets.UTF_8))); // prints: señor
System.out.println(decode("€100".getBytes(StandardCharsets.UTF_8))); // prints: €100
我有一堆旧的 AES 加密字符串,大致像这样加密:
- 字符串使用 ISO-8859-1 编码转换为字节
- 字节使用 AES 加密
- 结果转换为 BASE64 编码的字符数组
现在我想将新值的编码更改为 UTF8(例如,“€”不适用于 ISO-8859-1)。这意志的 如果我尝试使用 UTF-8 编码解密旧的 ISO-8859-1 编码值,当然会导致问题:
org.junit.ComparisonFailure: expected:<!#[¤%&/()=?^*ÄÖÖÅ_:;>½§@${[]}<|'äöå-.,+´¨]'-Lorem ipsum dolor ...> but was:<!#[�%&/()=?^*����_:;>��@${[]}<|'���-.,+��]'-Lorem ipsum dolor ...>
我正在考虑为此创建一些自动编码回退。
所以主要问题是是否足以检查解密的字符数组中的 '�' 字符以找出编码不匹配? 什么是 'correct' 比较时如何声明'�'符号?
if (new String(utf8decryptedCharArray).contains("�")) {
// Revert to doing the decrypting with ISO-8859-1
decryptAsISO...
}
解密时,你会得到原始的字节序列(你第一步的结果),然后你只能根据 ISO-8859-1 或 UTF-8 编码猜测这些字节表示字符。
从一个字节序列来看,没有办法清楚地说明它是如何被解释的。
一些想法:
- 您可以迁移所有旧的加密字符串(解密、使用 ISO-8859-1 解码为字符串、使用 UTF-8 编码为字节数组、加密)。那么问题一劳永逸地解决了。
- 你可以尝试解码两个版本的byte数组,看看一个版本是不是非法的,或者两个版本是否相等,如果还是不明确,就根据预期的字符取概率大的那个。我不建议这样做,因为它需要大量的工作,而且仍然有可能出错。
- 对于新条目,您可以在字符串/字节序列前添加一些未出现在 ISO-8859-1 文本中的标记。例如。有些人按照惯例在 UTF-8 编码文件的开头添加字节顺序标记。尽管生成的字节 (
EF BB BF
) 在 ISO-8859-1 中并非严格非法(读作
),但它们极不可能。然后,当您解密的字节以EF BB BF
开头时,使用 UTF-8 解码为字符串,否则使用 ISO-8859-1。尽管如此,还是有非零的错误概率。
如果可能的话,我会迁移现有条目。否则,您将不得不在您的代码库中永远使用“旧格式兼容性”,并且仍然不能绝对保证正确的行为。
将字节解码为文本时,不要依赖 �
字符来检测格式错误的输入。使用严格的解码器。这是一个辅助方法:
static String decodeStrict(byte[] bytes, Charset charset) throws CharacterCodingException {
return charset.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes))
.toString();
}
这里是对应的strict encoder helper方法,以备不时之需:
static byte[] encodeStrict(String str, Charset charset) throws CharacterCodingException {
ByteBuffer buf = charset.newEncoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.encode(CharBuffer.wrap(str));
byte[] bytes = buf.array();
if (bytes.length == buf.limit())
return bytes;
return Arrays.copyOfRange(bytes, 0, buf.limit());
}
由于 ISO-8859-1 允许所有字节,因此您不能使用它来检测格式错误的输入。然而,UTF-8 正在验证,因此它很可能检测到格式错误的输入。然而,这不是 100% 保证,但这是我们所能做到的最好的。
因此,尝试使用严格的 UTF-8 解码,如果失败则回退到 ISO-8859-1:
static String decode(byte[] bytes) {
try {
return decodeStrict(bytes, StandardCharsets.UTF_8);
} catch (CharacterCodingException e) {
return new String(bytes, StandardCharsets.ISO_8859_1);
}
}
测试
System.out.println(decode("señor".getBytes(StandardCharsets.ISO_8859_1))); // prints: señor
System.out.println(decode("señor".getBytes(StandardCharsets.UTF_8))); // prints: señor
System.out.println(decode("€100".getBytes(StandardCharsets.UTF_8))); // prints: €100