订单号的安全整数哈希


Secure integer hashing for order number

假设我有一个带有自动递增 id 的表Orders,例如 1、2、3、4...,它们当前查询为 http://www.example.com/order?id={1,2,3..}

现在,我想将主键 [1, 2, 3, ..] 散列到另一个称为订单号的数字中,以便我们的客户可以在他们的请求中引用它们,例如

1 -> 100192938303
2 -> 293029200002

我想要以下内容:

  • 无法通过查看自动增量 ID 来猜测我每天创建了多少订单
  • 不需要数据库额外的查找,纯粹由PHP进行哈希(和预定义的盐)
  • 无碰撞

可能吗?

我认为您可能会选择更简单的方法 - 不要使用自动递增 id,使用随机整数作为 id。例:

while (true) {
    $id = get_random_integer();
    $stmt = $db->prepare("INSERT INTO Orders (id, foo, bar) VALUES (:id, 'foo', 'bar')");
    try {
        $stmt->execute(array(':id' => $id));
        //OK
        break;
    } catch (Exception $ex) {
        if (is_duplicate_id_exception) {
            //generate new id and try again
            continue;
        }
        //Some other problem
        throw $ex;
    }
}

这样你就可以:

  • 避免碰撞
  • 不需要哈希函数和 {哈希 -> id} 映射
  • ID 不包含有关订单数量的信息

你提议使用加盐哈希。 由于哈希是单向函数,并且您需要从哈希转换为原始值,因此您需要以下方法之一将哈希转换为原始订单值:

  • 循环遍历合理的订单值,获取每个值的加盐哈希值,直到您确定匹配的哈希值或耗尽允许的订单 ID 池。
  • 缓存合理的订单值一次(例如,在应用程序启动时),并存储在哈希表中。 创建缓存后,此方法要快得多,但需要额外的查找。

您还注意到原始订单标识符是机密的,因为可以获得多个订单 ID 的攻击者可以推断订单量。 订单识别码的机密性与订单本身的机密性是分开的,该问题不涉及,可以通过单独的访问控制机制进行处理。

我认为您的示例中的首选方法是使用加密而不是哈希。 加密订单 ID 将满足机密性和往返要求,而不会产生哈希缓存或数据库查找的开销。 该方法可能如下所示:

  1. 使用您的密钥加密订单 ID。
  2. Base64 对订单 ID 进行编码,并作为令牌返回给客户端。
  3. 从客户端收到加密令牌后,解码 Base64 字符串
  4. 使用密钥解密解码的字符串以生成原始订单号。

例如,对于订单 42 和 DES 密钥E0EC4E44EF2C6CEE和零 IV,您将dmTt0kbIlcA=作为订单 ID 发送到客户端(如果将 42 编码为小端序 32 位整数)。 (此处适合使用零 IV,因为在您的方案中不考虑唯一的密文。

这里有两个想法:

  1. 使用可逆哈希。 这是否有效取决于您认为的安全性,因为它本质上只是混淆。 但是,如果您自定义它(可能会更改算法中某些步骤的顺序),并且防止源代码泄露,它将阻止除最坚定的攻击者之外的所有攻击者。 (根据您的安全目标,您可能希望与其他一些技术相结合以降低泄漏风险,例如,员工离开公司。 考虑将算法的一部分保密,就好像它是加密密钥一样,并对输入进行额外的可变预转换。

在我的头顶上,一个简单的可逆哈希可能只是位的"横向添加"。 对于更复杂的东西,在我的头顶上,流行的"MurmurHash"算法家族据称是可逆的。

我不知道有任何加密强度高的可逆哈希。 但是,关于对称加密主题的其他答案与此想法相似。

  1. 使用流密码,又名加密 RNG。 如果订单总数相当小,这是合适的。 您需要的是一个独特的数字序列,它与计数数字序列一对一映射。 因此,使用您选择的 RC4 或 HMAC 生成一个唯一的随机数序列,随时消除重复项。 (也许让这一切快速进行的一种创造性方法是布隆滤镜。

对于从内部 ID 到外部 ID 的映射,您只需生成序列。 相反,您继续前进,直到找到 ID 或达到最大订单 ID。 这个算法是O(n),这显然不理想,但如果你愿意妥协一点,增加更多的复杂性,或者聪明一点,你可能会找到一种方法来缓解这种情况。 例如,您可以在RAM中保留ID的缓存。

编辑:

由于线性复杂性,我自己对#2持怀疑态度,所以我运行了一些数字。 使用 Core2 处理器的 Crypto++ 基准数字,如果您预算 10 毫秒进行数字转换,并且使用 40 位 ID(假设达到一千万亿个订单),则订单 ID 最大值约为 2,500,000。 我认为你可以通过使用较小的键将其加倍。

所以这种方法可以采用任何一种方式。 对于小规模的东西,这很好。 (上述假设是保守的。 但对于大规模的东西,这可能是一个烦恼。 这足以让您完成产品发布;在你开始讨论如何将你的软件构建为分布式系统的时候,你会想重新审视它,这也将有助于解决这个问题。 但在这一点上,你最好质疑最初的假设,只是把这个东西存储在某个地方的数据库中。

您可以在将

订单 ID 提交到 GET 表单之前使用 base64_encode() 对其进行编码,然后在捕获表单发送的变量时使用 base64_decode() 进行编码

您甚至可以添加盐,例如base64_encode($id)。盐")