谷歌验证器是如何工作的?(一)

更多互联网精彩资讯、工作效率提升关注【飞鱼在浪屿】(日更新)


通过 WEB 访问服务时——以GMail为例——有几件事必须预先发生:

  1. 您要连接的服务器(GMail)必须了解您的身份。
  2. 只有在了解您是谁之后,它才能决定您可以访问哪些资源(例如您自己的电子邮件收件箱、您的日历网盘等)。

上面的第 1 步称为身份验证。第2步是授权(服务器认证成功后才能授权)。

使用Google Authenticator 之类的应用程序就是第 1 步。您可以将这一步视为登录您的GMail帐户。


解决什么问题?

通常使用以下一种(或多种)方法执行身份验证:

  1. 你知道什么?– 服务器通过询问只有您应该知道的信息(例如用户名和密码)来“测试您”。这是最常见的方法。您会看到一个登录表单,您可以在其中输入凭据。
  2. 你有什么?– 服务器通过确保您拥有一些您应该拥有的东西来测试您(例如, 在您的智能手机上安装的Google Authenticator 的内部嵌入了某种secret/秘密)。只有您应该可以物理访问您的手机。如果不这样做(换句话说,您无法打开应用程序并输入代码),身份验证将失败。
  3. 您是谁– 服务器会测试您的生物识别信息。这可以使用智能手机/笔记本电脑中的指纹读取器、iPhone 中的面部 ID 等来完成。

多重身份验证( MFA ) 是关于使用上述两种或更多方法。通常,您会与例如Google Authenticator (或其他应用程序)一起设置用户名/密码。当然,对于你所拥有的方法,你有更多的选择。例如 FIDO2,我觉得它更方便。但在这里,我们专注于 使用称为TOTP算法的应用程序,例如Google Authenticator

我们想要实现的是了解Google Authenticator(或任何其他 TOTP 应用程序)这样的应用程序是如何解决这个问题的?


如何让你的智能手机成为必需品?

我们必须提出一种算法,以证明用户在进行身份验证时可以物理访问运行该应用程序的智能手机。我们怎么做?

首先想到的是,让我们在手机应用程序中嵌入某种秘密——例如密码。然后,每次用户登录时,让我们要求用户打开应用程序并输入应用程序显示的密码。

例如,我们的应用程序有一个密码:$3cr3tP4$$在里面。当我们登录时,服务器(GMail)要求我们提供用户名和密码。我们输入它。然后,作为第二步,服务器要求我们输入应用程序显示的密码并将其输入到网页上。如果我们在第一步中输入了正确的用户名和密码,然后我们输入了 $3cr3tP4$$——因为这是应用程序向我们展示的——我们已经成功通过身份验证,因此,登录。

现在,您可能会立即看到这一思路中的一个大漏洞。如果我们只是被要求从应用程序中输入相同的密码,那么没有什么可以阻止我们从应用程序中取出密码(写在一张纸上,记住它等)。从那时起,这里不再需要您的手机。这第二个因素只是对第一个因素的扩展(静态密码的衍生品)。

一种利用它的方法是以下场景:

  1. 有人(黑客)在你用来登录的机器上安装了一个键盘记录器。
  2. 他们从第一个因素中捕获您的用户名/密码。
  3. $3cr3tP4$$从第二个因素捕获静态密码 ( )。

现在他们拥有了登录您的 GMail 帐户所需的所有凭据。

那么问题就变成了:我们如何证明手机包含该静态秘密(例如,密码$3cr3tP4$$:)而不泄露它?


散列

我们可以使用对像散列函数MD5(不建议使用MD5,它太容易被发现的碰撞攻击)SHA-1, SHA-512等等。这些功能变换输入(任何规模的输入)到一个标准,具体为每这些功能, 字节串。既然Google Authenticatot和与之兼容的应用程序与 一起使用SHA-1,我们也可以使用它。

如果转换秘密 ( $3cr3tP4$$) SHA-1,将获得以下字节序列(以十六进制表示法):

e2 56 28 6a fb 11 fd b7 40 1f 9a f5 30 73 39 75 dc 7d c4 64.

只需运行以下命令:

$ echo -n '$3cr3tP4$$' | sha1sum
e256286afb11fdb7401f9af530733975dc7dc464 -

现在 – 再次 – 哈希(任何哈希,不仅是SHA-1)对于$3cr3tP4$$. 如果我们期望总是只输入那个哈希值,或者它的一部分,因为它很长,我们总是必须输入完全相同的东西。7我们会将每次输入相同的密码替换为每次输入相同的哈希值。我们仍然可以轻松地得哈希并将其写在一张纸上。

使用散列函数听起来是一个好的开始,但我们必须以某种方式每次都改变它,但改变的方式只有知道秘密的人才能预测。唯一知道这里秘密的是:GMail服务器和手机上的应用程序(例如Google 身份验证器)。

那么我们如何让它每次都以一种只有真正知道秘密的人才能预测的方式发生变化呢?


撒少许盐

如果我们每次都可以在密码中添加一些东西并从中计算哈希值,那么每次哈希值都会不同。此外,如果同时,服务器和应用程序,可以同步在什么被加入到密码,我们就能够实现其散列值是每次都不同,并在可预见的方式。例如,我们可以使用以下算法(应用程序端 1):

  1. 在 0 处启动计数器(此步骤最初仅执行一次)。
  2. 附加计数器的 ASCII 表示(在第一遍的结果是:)0$3cr3tP4$$。
  3. 计算上面的散列(最初0$3cr3tP4$$,结果是用ASCII表示 – 41bb78b5b4963c5c78c79702b47d414d17d9127e)。
  4. 将计数器加 1。
  5. 向用户 ( d9127e)显示最后几个字节。
  6. 用户输入d9127e网络表单。

在服务器端:

  1. 在 0 处启动计数器(此步骤最初仅执行一次)。
  2. 附加计数器的 ASCII 表示(在第一遍的结果是:)0$3cr3tP4$$。
  3. 计算上面的散列(最初0$3cr3tP4$$,结果是用ASCII表示 – 41bb78b5b4963c5c78c79702b47d414d17d9127e)。
  4. 将计数器加 1。
  5. 获取上述 SHA-1 和 ( d9127e)的最后 3 个字节(ASCII 表示)。
  6. 将这 3 个字节与用户在 WEB 表单中输入的内容进行比较。如果两者匹配,则认证成功。如果不是,则身份验证失败。

第 2 次在计数器周围会是 1,所以双方都会计算1$3cr3tP4$$. 结果将是efbffb4295680f674d5c061cb60103bbc7d6fca2(last 3 bytes d6fca2)。所以现在代码每次都以一种可预测的方式改变。但只有当你知道秘密 ( $3cr3tP4$$) 时,它才是可预测的。如果不这样做,您将不知道如何计算哈希值。即使您知道计数器的值,您也无法复制哈希值。

向散列的任何内容添加少量额外内容的方法称为salting,这是一种非常流行的技术。例如,如果您正在运行 GNU/Linux,它也会在您的/etc/shadow 中使用。查看密码的散列值是什么样的。然后使用passwd 命令修改密码,但不要真正更改它——输入您一直使用的相同密码。然后 再次检查/etc/shadow中您的(不是)新密码的散列表示。即使您的密码并没有真正改变,哈希值也会有所不同。那是因为您的系统在计算哈希值并将其存储在/etc/shadow之前对密码进行了加盐处理。

Google Authenticator这样的应用程序的关键就是使用这个想法。取 2 个组件:秘密(只有服务器和应用程序知道)和某种形式的计数器(不是真正机密),以某种方式将它们组合起来,然后计算哈希值。接下来,我们将查看有关该过程的所有技术细节。或者我应该说流程——实际上我们将描述两个标准:HOTPTOTP


认识 HMAC

将秘密与计数器相结合以每次产生唯一的想法——但只能服务器和应用程序可以进行预测——哈希很难以一种抵抗各种攻击向量的方式正确获取,这会以某种方式使其更容易预测下一个哈希。围绕该概念地址安全称为HMAC(Hash-based Message Authentication Code)。该算法描述了将密钥(在我们的示例中 $3cr3tP4$$)与消息(在我们的示例中是计数器)结合起来并在您不知道密钥的情况下以安全且难以预测的方式计算 HASH 的过程。

考虑HMAC 的另一种方式是名称的实际含义——使用预先共享的秘密以加密方式签署消息(在我们的例子中是计数器$3cr3tP4$$)。因此,您的应用正在使用该密码对计数器进行签名。并且只有服务器能够验证签名,因为唯一知道密码的其他实体是服务器。

Google Authenticator和其他与其兼容的身份验证器应用程序实际上使用HMAC-SHA-1——一种使用SHA-1计算哈希值的HMAC。因此,让我们在本文的其余部分坚持使用SHA-1

  • K——
  • K' – 为满足HMAC算法的需要而处理成的密钥
  • m – 消息(在我们的例子中是计数器)
  • H(…) – 这是一个散列函数(例如SHA-1
  • +(圆圈中的 +)——异或运算
  • opad – 外填充键(见下面的详细信息)
  • ipad – 内填充键(见下面的详细信息)
  • || – 串联

简而言之,首先我们要对密钥进行预处理。HMAC 要求密钥具有特定长度。它的大小必须等于散列算法的块大小或其结果大小。例如,对于64 字节或 20 字节的SHA-1。嗯,大多数情况下,您选择的键会更短或更长。因此,预处理是使密钥恰好为 64 或 20 个字节长。如果您的密码较短($3cr3tP4$$非常短),您只需在末尾添加零以使其长度为 64 字节。如果密钥正好是 64 字节长,那么你什么都不做——它已经很好了。如果它更长,则计算 SHA-1并将其用作密钥(这里它的大小将只有 20 个字节)。

接下来,在计算opadipad时进行一些异或运算,并在此过程中使用对已处理的键进行异或运算。这是关键部分。如果您不知道秘密,您就不知道对上述字节进行 XOR 什么,因此您将无法获得最终结果 – 最终哈希。

然后用ipad散列 XOR 键的结果并与消息(计数器)连接。接下来,您将opad XORing的结果与上述内容连接起来并对其进行散列。结束——这是HMAC的最终结果。


关于长键的题外话

请注意,当密钥的长度为 40 字节时,它会用零填充以使其长度为 64 字节。因此该密钥的实际熵相当于 40 字节长的密钥。但是如果密钥长于 64 个字节,它实际上被缩短为 20 个字节。所以它的熵较低。如果您想最大化密钥的复杂性, 但不要超过64 字节。


快速代码示例

可能更愿意查看代码来帮助您理解它。下面编造了一个超短的 Python 脚本来说明HMAC的优雅和简单(尤其是HMAC-SHA-1风格)。

在下面的代码中,您可以看到,我正在使用我们的密码 ( $3cr3tP4$$)签署初始计数器值(仍然是 0 的 ASCII 表示)。你可以在网上搜索一个HMAC计算器来验证它是否真的能完成这项工作。

#! /usr/bin/env python3

import hashlib

# Our secret known only to the server and the app
secret = "$3cr3tP4$$"

# Counter. Here it's simply an ASCII string representation of the counter
# value. Initially it's "0"
initial_counter = "0"

# In HMAC-SHA-1 we're using SHA-1 hash function. In SHA-1 specifically
# the block size is 64 bytes. And the result is 20 bytes long.
sha_1_block_size_bytes = 64
sha_1_result_size_bytes = 20

def prepare_key(key):
if len(key) > sha_1_block_size_bytes:
# When the key is longer than 64 bytes, just hash it (in
# HMAC-SHA-1 using SHA-1).
return hashlib.sha1(key).digest()
elif len(key) == sha_1_block_size_bytes:
# When the key is exactly 64 bytes long, don't do anything.
# No processing needed.
return key
else:
# If key is shorter than 64 bytes, pad it with zeros at the end.
result = key
while len(result) < sha_1_block_size_bytes:
result += b"0"
return result

def pad(processed_key, padding_byte):
# Processed key is already exactly 64 bytes long.
result = list()
for ch in processed_key:
# Every byte of the key is being XORed with padding_byte (either
# 0x5c for the outer pad or 0x36 for the inner pad).
b = ch ^ padding_byte
result.append(b)

return bytes(result)

def hmac_sha_1(key, counter):
key = prepare_key(bytearray(key, "utf-8"))
# outer_key_pad is basically an opad from HMAC's definition on
# the Wikipedia:
# https://en.wikipedia.org/wiki/HMAC#Definition
outer_key_pad = pad(key, 0x5c)
# Similarly, inner_key_pad is ipad (same link as above)
inner_key_pad = pad(key, 0x36)

inner = inner_key_pad + bytearray(counter, "utf-8")
inner_hashed = hashlib.sha1(inner).digest()

return hashlib.sha1(outer_key_pad + inner_hashed)

print(f'Secret: "{secret}"')
print(f'Initial counter: "{initial_counter}"')
# hexdigest() returns an ASCII representation of the hash in HEX
# notation (each byte is a hexadecimal number).
print(f'HMAC-SHA-1: "{hmac_sha_1(secret, initial_counter).hexdigest()}"')

此脚本的输出:

Secret: "$3cr3tP4$$"
Initial counter: "0"
HMAC-SHA-1: "5d1014482edb0afb42101d8d4b5ff9bb5340a683"

现在,你有了。我们已经非常接近了解身份验证器应用程序如何工作的细节。我们需要弄清楚一些细节,比如如何在HMAC 中表示计数器。他们不使用 ASCII 表示。另一个问题是保持服务器和应用程序之间的计数器同步。我们将在本文的第二部分解决这些问题。它将专注于这些身份验证器应用程序使用的 2 种算法的具体细节:HOTPTOTP

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据