将字节数组转换为字符串,然后再转回来的结果不同

6

我正在使用libsodium的 .net 端口。哈希生成函数有两种形式,一种接受字节数组,一种接受字符串:

public static byte[] ArgonHashBinary(string password, string salt, long opsLimit, int memLimit, long outputLength = ARGON_SALTBYTES)  

public static byte[] ArgonHashBinary(byte[] password, byte[] salt, long opsLimit, int memLimit, long outputLength = ARGON_SALTBYTES)

我遇到的问题是当输入值相同时,两个表单产生相同的哈希值。

var saltAsBytes = PasswordHash.ArgonGenerateSalt();
var saltAsString = Encoding.UTF8.GetString(saltAsBytes);
var tmp = Encoding.UTF8.GetBytes(saltAsString);

var hash1 = PasswordHash.ArgonHashBinary(password, saltAsString, 6, 134217728, 16);
var hash2 = PasswordHash.ArgonHashBinary( Encoding.UTF8.GetBytes(password), saltAsBytes, 6, 134217728, 16);

"PasswordHash."开头的内容都是libsodium而不是我的代码。

从上面的代码中,当我将其从字符串转换为字节数组,然后再将其转换回来时,字节数组的长度总是不同的。ArgonGenerateSalt()生成一个长度为16的字节数组。当我将其从上面的字符串转换回来时,它通常是 ~30(每次因生成不同盐而不同)。

为什么要转换为UTF8?因为这是它们在内部所做的事情: https://github.com/adamcaudill/libsodium-net/blob/master/libsodium-net/PasswordHash.cs

public static byte[] ArgonHashBinary(string password, string salt, StrengthArgon limit = StrengthArgon.Interactive, long outputLength = ARGON_SALTBYTES)
    {
      return ArgonHashBinary(Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(salt), limit, outputLength);
    }

当我将盐转换为UTF8字符串时,哈希函数会失败,因为它们检查字节数组的长度以确保其为16个字节。如果我将其转换为ASCII字符串,则可以工作,但会产生不同的哈希值(这是预期的)。
需要澄清的是,此代码中的哈希部分不是问题所在。找出为什么“tmp”与“saltAsBytes”不同是关键。

你是否单步调试了PasswordHash中的代码,以查看问题出在哪里? - Jim Mischel
我认为这不是编码类型的问题。如下面的答案所述,您无法将任意字节数组转换为字符串,然后再转回来并确保获得相同的结果。@codin4fun:此外,该库也不会这样做。通常它使用字节数组,并且只有在进一步处理之前才有额外的方法,可以将字符串转换为字节数组。 - H.G. Sandhagen
1
@Theo: 钱在哪里?:-) var bytes = new byte[] { 255, 255, 255 }; var buf = Encoding.Unicode.GetString(bytes); var newbytes = Encoding.Unicode.GetBytes(buf); var n = String.Join(", ", newbytes); 返回 255,255,253,255 - H.G. Sandhagen
问题是为什么他们会有一个“字符串”版本的哈希函数?为什么他们允许盐以字符串形式传递(而不是字节数组)?如果我有一个哈希库和一个盐生成函数,我希望每个人都使用它而不是自己编写的。我的想法有任何错误吗? - coding4fun
1
@Theo,请查看我的答案中的测试用例,并将钱捐赠给一位无家可归者。 - L.B
显示剩余6条评论
2个回答

3
我认为这里的问题在于ArgonGenerateSalt方法没有返回一个UTF8编码的字符串,而是返回完全随机的字节
你不能将随机字节解码为UTF8字符串并期望它能够往返。一个简单的例子可以看到这个问题是这样产生的:
var data = new byte[] { 128 };
var dataAsString = Encoding.UTF8.GetString( data );
var dataAsBytes = Encoding.UTF8.GetBytes( dataAsString );

在此之后,dataAsBytes将会是3个字节(具体来说是239、191、189)。

1
这让你认为ArgonHashBinary字符串版本的“salt”参数永远无法由库生成。你必须自己生成它。如果是真的,似乎是一个糟糕的设计。 - coding4fun
@coding4fun,这确实似乎是一个重大的疏忽。 - Kyle

3

将字节数组转换为字符串,然后再转回字节数组产生了不同的结果

二进制数据可能无法使用Encoding.[任何编码].GetBytesEncoding.[任何编码].GetString方法将其转换为字符串,然后再转回字节数组。

而应该使用Convert.ToBase64StringConvert.FromBase64String方法。

您可以轻松测试...

var bytes = new byte[] { 255, 255, 255 }; 
var buf = Encoding.UTF8.GetString(bytes);
var newbytes = Encoding.UTF8.GetBytes(buf);

newbytes的长度将为9.....

编辑: 这是@Theo的测试用例

var bytes = new byte[] { 0, 216 }; //any new byte[] { X, 216 };
var buf = Encoding.Unicode.GetString(bytes);
var newbytes = Encoding.Unicode.GetBytes(buf); //253,255

其实在提问之前我也有这个想法,但不幸的是它行不通。当将字符串传递给哈希函数时,长度将!= 16,这会导致'SaltOutOfRangeException'异常发生。 - coding4fun
@coding4fun,我不明白你的意思,但是你需要一个可逆的函数来在字符串和任意二进制数据之间进行转换。使用编码不是正确的方法...你还有其他选项,比如BitConverter.ToString或者SoapHexBinary,但是Base64编码是最自然的选择... - L.B
@coding4fun 顺便说一下:您应该对长度为16的字节数组进行编码并转换为Base64字符串… 其反向操作将再次得到一个由16个字节组成的字节数组。 - L.B
1
在将盐传递到 ArgonGenerateSalt 的“字符串”版本并在内部转换为“UTF8”之后,它必须是16个字节长。将库生成的盐转换为Base64字符串会导致其长度> 16,从而引发异常。 - coding4fun

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