在Ruby中将独特的种子字符串转换为随机但确定性的浮点数值

17

这对我来说在概念上有些困难。

基本上,我需要接受一些任意的唯一字符串,并能够将其转换为标准化的浮点值。输出的浮点值实际上并不重要,只要相同的字符串输入始终产生相同的标准化浮点输出即可。

所以这是一个哈希算法,对吧?我熟悉 SHA1 或 MD5,这似乎类似于密码哈希,其中正确密码的结果是相同的。但我相信这些方法输出的是一串字符,我不明白的是如何将 SHA1 或 MD5 的结果转换为一致的浮点值。

# Goal
def string_to_float(seed_string)
  # ...
end

string_to_float('abc-123') #=> 0.15789
string_to_float('abc-123') #=> 0.15789

string_to_float('def-456') #=> 0.57654
string_to_float('def-456') #=> 0.57654

在Ruby中,我可以采用什么样的方法将任意字符串转换为随机但一致的浮点数值?


你想要结果是"安全的"吗?也就是说,那些拥有浮点数的人无法猜测出原始字符串是什么?还是这个问题并不重要? - emboss
1
安全性不是问题。只要任何唯一的输入产生与输出相同的标准化浮点数即可。但即使存在问题,似乎可以轻松添加一个秘密盐,我已经掌握了这种方法的基础知识。 - Alex Wayne
3个回答

23

你需要的关键部分是将SHA1或MD5哈希输出转换为既确定性又一对一的浮点数。以下是一个基于md5的简单解决方案。这也可以用作整数。

require 'digest/md5'

class String
  def float_hash
    (Digest::MD5.hexdigest(self).to_i(16)).to_f
  end
end

puts "example_string".float_hash  # returns 1.3084281619666243e+38

这会生成一个十六进制哈希值,然后将其转换为整数,最后将其转换为浮点数。每个步骤都是确定性的。

注意:正如@emboss指出的那样,这降低了碰撞抵抗力,因为双精度浮点数占用8字节而哈希值占用16字节。但根据您的应用程序的描述,这可能不是什么大问题。


碰撞抵抗力与哈希不同,因为浮点值的大小受限 - 在内部表示为双精度,并且MD5已经具有16字节的输出。对于OP来说,这可能不会有影响,但在加密术语中,这是一个巨大的差异。 - emboss
@emboss:哎呀,你说得很对。我错误地假设 size(double) >= size(md5_hash) - 显然是错误的。我会更新我的答案。 - Peter
没问题,我一开始也是这么想的 ;) - emboss

5
如果安全不是问题,我认为你描述的内容不是哈希函数。哈希函数是一种单向函数,意味着计算哈希很容易,但还原哈希很“困难”,理想情况下是不可能的。
相反,你的要求描述了一个单射函数。对于你定义域X中的任何x1、x2,以下条件成立:
For all x1, x2 element of X, x1 != x2  => f(x1) != f(x2)

f(x)=x是这样一个函数,f(x)=x²不是。简单来说:你希望在输入不同的情况下得到不同的结果,在输入相同的情况下得到相同的结果。对于安全哈希来说也是如此,但它们还提供了单向特性,例如只通过f(x)很难(容易地)找到x等属性。据我所知,您不需要这些安全特性。

显然,从字符串到浮点数的这种可逆映射将由将“字符串字节”解释为“浮点字节”来给出,即您以后以不同的方式解释字节(想想C:

unsigned char *bytes = "...";
double d = (double)bytes; 

但是,这种方法也有缺点——浮点数具有最大精度,所以如果你的字符串太长,就会遇到溢出情况(在32位机器上,浮点数内部表示为double值,即8个字节)。因此,对于几乎任何用例来说,空间都不足。即使先对字符串进行MD5处理,也无法解决问题——MD5输出已经是16个字节长了。

因此,这可能是一个真正的问题,具体取决于您的确切要求。虽然MD5(或任何其他哈希)会足够地混淆输入,使其尽可能随机,但您仍然将可能值的范围从16个字节削减到实际上的8个字节。(注意:在保留随机性方面,将随机的16字节输出截断为8字节通常被认为是“安全”的。椭圆曲线加密也采用了类似的方法。但据我所知,没有人真正证明过它,但也没有人能证明相反)。因此,在受限的Float范围内发生碰撞的可能性更大。根据生日悖论,找到碰撞需要sqrt(有限范围内的值数量)次尝试。对于MD5,这是2^64,但对于您的方案,只有2^32。这仍然非常非常不可能产生碰撞。这可能类似于同时中彩票和被闪电击中的几率。如果您可以接受这种微小的可能性,请继续使用:

def string_to_float(str)
  Digest::MD5.new.digest(str).unpack('D')
end

如果唯一性是绝对优先考虑的话,我建议从浮点数转换为整数。Ruby支持大整数而不受long值的内部限制(那就是Fixnum的本质)。因此,任何任意哈希输出都可以表示为一个大整数。

4
是的,你在描述一个哈希算法。你可以使用MD5或SHA1摘要(因为它们只产生随机位),通过使用String#unpack方法并将参数设置为"G"(双精度浮点数,网络字节顺序)从摘要生成一个浮点数:
require 'digest/sha1'

def string_to_float(str)
  Digest::SHA1.digest(str).unpack("G")[0]
end

string_to_float("abc-123") # => -2.86011943713676e-154
string_to_float("def-456") # => -1.13232994606094e+214
string_to_float("abc-123") # => -2.86011943713676e-154 OK!
string_to_float("def-456") # => -1.13232994606094e+214 OK!

请注意,如果您希望结果浮点数在特定范围内,则需要进行一些调整。
另请注意,未打包的数字并未使用来自摘要的所有位,因此您可能希望将其组合成双精度浮点数的字节数(尽管如果您关心哈希函数的熵不降低,您必须小心),例如:
def str2float(s)
  d = Digest::SHA1.digest(s)
  x, y = d[0..9], d[10..19]
   # XOR the 1st (x) and 2nd (y) halves to use all bits.
  (0..9).map {|i| x[i] ^ y[i]}.pack("c*").unpack("G")[0]
end

有趣。我感觉这是一个二进制打包/解包,但不知道如何实际使用这些方法。 - Alex Wayne

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