Node.js与.NET字符串编码差异:密码哈希移植适配问题
.NET与Node.js UTF8字节数组转字符串再转回的一致性问题解决
差异原因
问题核心在于两者处理无效UTF8字节序列的替换逻辑不同:
- Node.js默认的UTF8解码器(
Buffer.toString('utf8'))遵循WHATWG编码标准,遇到连续无效字节时,会将整个序列替换为单个U+FFFD替换字符(对应字节239,191,189)。 - .NET的
UTF8Encoding.GetString()默认行为是将每个单独的无效字节都替换为一个U+FFFD替换字符,导致解码后的字符串存在差异,最终编码回字节数组的结果自然不一致。
解决方案
要在Node.js中复现.NET的逻辑,需要手动模拟.NET的UTF8解码规则,逐个处理每个字节并替换无效值:
模拟.NET的UTF8解码函数
function decodeUtf8LikeDotNet(buffer) { const result = []; let i = 0; const length = buffer.length; while (i < length) { const byte = buffer[i]; if (byte <= 0x7F) { // 单字节合法字符,直接添加 result.push(String.fromCharCode(byte)); i++; } else if (byte >= 0xC0 && byte <= 0xDF) { // 双字节起始,检查后续续字节 if (i + 1 < length && buffer[i+1] >= 0x80 && buffer[i+1] <= 0xBF) { const charCode = ((byte & 0x1F) << 6) | (buffer[i+1] & 0x3F); result.push(String.fromCharCode(charCode)); i += 2; } else { result.push('\uFFFD'); i++; } } else if (byte >= 0xE0 && byte <= 0xEF) { // 三字节起始,检查后续两个续字节 if (i + 2 < length && buffer[i+1] >= 0x80 && buffer[i+1] <= 0xBF && buffer[i+2] >= 0x80 && buffer[i+2] <= 0xBF) { const charCode = ((byte & 0x0F) << 12) | ((buffer[i+1] & 0x3F) << 6) | (buffer[i+2] & 0x3F); result.push(String.fromCharCode(charCode)); i += 3; } else { result.push('\uFFFD'); i++; } } else if (byte >= 0xF0 && byte <= 0xF7) { // 四字节起始,检查后续三个续字节 if (i + 3 < length && buffer[i+1] >= 0x80 && buffer[i+1] <= 0xBF && buffer[i+2] >= 0x80 && buffer[i+2] <= 0xBF && buffer[i+3] >= 0x80 && buffer[i+3] <= 0xBF) { const charCode = ((byte & 0x07) << 18) | ((buffer[i+1] & 0x3F) << 12) | ((buffer[i+2] & 0x3F) << 6) | (buffer[i+3] & 0x3F); result.push(String.fromCharCode(charCode)); i += 4; } else { result.push('\uFFFD'); i++; } } else { // 无效字节(如单独的续字节),替换为U+FFFD result.push('\uFFFD'); i++; } } return result.join(''); }
使用函数生成匹配结果
const password = 'Asdfgh@'; const saltByteArray = Buffer.from([250, 106, 6, 180, 111, 229, 165, 162, 186, 86, 137, 89, 153, 76, 94, 105, 241, 1, 50, 174, 65, 119, 212, 250, 152, 244, 166, 24, 195, 245, 235, 9]); // 模拟.NET解码盐字节数组 const saltStr = decodeUtf8LikeDotNet(saltByteArray); const str = saltStr + password; const pwdByteArr = Buffer.from(str, 'utf8'); // 验证结果是否与预期一致 const expected = Buffer.from([239, 191, 189, 106, 6, 239, 191, 189, 111, 229, 165, 162, 239, 191, 189, 86, 239, 191, 189, 89, 239, 191, 189, 76, 94, 105, 239, 191, 189, 1, 50, 239, 191, 189, 65, 119, 239, 191, 189, 239, 191, 189, 239, 191, 189, 239, 191, 189, 24, 239, 191, 189, 239, 191, 189, 239, 191, 189, 9, 65, 115, 100, 102, 103, 104, 64]); console.log(pwdByteArr.equals(expected)); // 输出true
优化建议
如果不需要严格复现.NET的字符串转换中间步骤,更合理的做法是直接拼接盐字节数组和密码的UTF8字节数组——跳过字符串转换可以避免无效UTF8字节带来的信息丢失,同时更符合哈希算法的输入要求。仅当需要兼容已存储的旧哈希值时,才需要使用上述模拟解码的方法。
内容的提问来源于stack exchange,提问作者zolipapa
相关产品推荐
相关产品推荐

