You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 04:45:00