Google 验证器代码与服务器生成的代码不匹配
Google Authenticator code does not match server generated code
背景
我目前正在开发一个双因素身份验证系统,用户可以使用他们的智能手机进行身份验证。在用户可以使用他们的设备之前,他们需要先验证它。为此,他们需要扫描我给他们的二维码并输入随后显示的代码。
问题
二维码扫描工作正常,Google Authenticator 应用程序可以正确读取它。但是,生成的代码与我在服务器上生成的代码不匹配。
我尝试了什么
我已经尝试了几件事,希望能找到我的问题。
我试过直接插入两个默认密码:
'thiswasmysecretkeyused'
和秘密的 base64.b32encode()
编码版本:
'ORUGS43XMFZW26LTMVRXEZLUNNSXS5LTMVSA===='
在 Google Authenticator 应用程序中,但这两个生成的代码与服务器不同。
我读到密钥的尾随 ====
可能会导致它不起作用,所以我也尝试添加一个没有这些的密钥。仍然没有好的结果(他们生成相同的代码)
我尝试使用不同的算法生成 TOTP 代码,因为在不太可能发生的情况下,我正在使用的算法(django-otp) is incorrect. The different algorithm I used was taken from this 答案。两种算法在使用时生成相同的代码相同的密钥。
我检查了系统上的时间。我看到操作系统显示 15:03
就像我的智能手机一样。在使用 time.time()
和 datetime.datetime.now()
转储 python 中的时间后,我看到返回的时间比操作系统时间晚一小时;显示 14:03
。我尝试在用于代码生成的时间戳中添加 3600
秒,但无济于事。
我尝试过其他几种方法,但不太记得它们是什么。
我在 Google Authenticator 中查找了接受密钥的代码,并验证它需要一个 base32 字符串。因此,据我所知,我对密钥的编码是正确的。来自代码(EnterKeyActivity.java,第 78 行):
Verify that the input field contains a valid base32 string
代码
生成密钥;
def generate_shared_key(self):
# create hash etc.
return base64.b32encode(hasher.hexdigest())
正在生成二维码;
key = authenticator.generate_shared_key()
qrcode = pyqrcode.create('otpauth://totp/someurl.nl?secret=' + key)
正在生成 TOTP 代码;
def generate_code(self, drift_steps=0, creation_interval=30, digits=6, t0=0):
code = str(totp(self.generate_shared_key(), creation_interval, timestamp, digits, drift_steps))
return code.zfill(digits)
如果您需要更多代码,例如 django-otp 实际的 totp 生成代码,请告诉我。
错误
没有错误。
预感
我的直觉是,我一定是在密钥生成或将密钥传递给 Google Authenticator 的某个地方出错了。因为即使手动将密钥放入 Google Authenticator 也无法生成正确的代码。 Google Authenticator 是否在保存密钥后对其执行更多操作,例如添加用户?
我还注意到在我使用的另一个算法中,那里的秘密首先被解码;
key = base64.b32decode(secret, True)
我的原始密钥(SHA512 哈希)有误吗?我应该还是不应该使用 base64.b32encode()
对其进行编码?如果我尝试扫描生成的 QR 代码而不对哈希进行编码,Google Authenticator 说它无法将其识别为(有效)密钥。
好吧,经过the code of Google Authenticator的挖掘,我终于找到了我做错了什么。
密钥编码
很清楚:Google验证器是否期望base32
编码字符串作为秘密。因此,无论您是手动输入还是通过二维码输入,当您将密码提供给 Google Authenticator.
时,都必须确保您的密码是 base32
编码的字符串
来自 EnterKeyActivity:
/*
* Verify that the input field contains a valid base32 string,
* and meets minimum key requirements.
*/
private boolean validateKeyAndUpdateStatus(boolean submitting) {
//...
}
存储
Google 身份验证器将您提供的密钥按原样存储在数据库中。所以这意味着它将 base32
字符串直接存储在数据库中。
来自 EnterKeyActivity:
private String getEnteredKey() {
String enteredKey = mKeyEntryField.getText().toString();
return enteredKey.replace('1', 'I').replace('0', 'O');
}
protected void onRightButtonPressed() {
//...
if (validateKeyAndUpdateStatus(true)) {
AuthenticatorActivity.saveSecret(this, mAccountName.getText().toString(), getEnteredKey(), null, mode, AccountDb.DEFAULT_HOTP_COUNTER);
exitWizard();
}
//...
}
static boolean saveSecret(Context context, String user, String secret, String originalUser, OtpType type, Integer counter) {
//...
if (secret != null) {
AccountDb accountDb = DependencyInjector.getAccountDb();
accountDb.update(user, secret, originalUser, type, counter);
//...
}
}
检索
当 Google Authenticator 从数据库中检索到秘密时,它会解码 base32
字符串,以便它可以使用真正的秘密。
来自 OtpProvider:
private String computePin(String secret, long otp_state, byte[] challenge) throws OtpSourceException {
//...
try {
Signer signer = AccountDb.getSigningOracle(secret);
//...
}
}
来自 AccountDb:
static Signer getSigningOracle(String secret) {
try {
byte[] keyBytes = decodeKey(secret);
//...
}
}
private static byte[] decodeKey(String secret) throws DecodingException {
return Base32String.decode(secret);
}
错误
我的错误是,在服务器端,我使用 base32
编码的密钥来生成 TOTP 代码,因为我认为 Google Authenticator 也使用它。事后看来,这当然是非常合乎逻辑的,但我找不到太多关于此的信息。希望这会在未来帮助更多人。
TL;DR
确保您传递给 Google 验证器的 secret/key 是一个 base32
编码字符串。确保在服务器端您使用的不是 base32
编码字符串,而是解码字符串。在 Python 中,您可以按如下方式对 secret/key 进行编码和解码:
import base64
base64.b32encode(self.key)
base64.b32decode(self.key)
背景
我目前正在开发一个双因素身份验证系统,用户可以使用他们的智能手机进行身份验证。在用户可以使用他们的设备之前,他们需要先验证它。为此,他们需要扫描我给他们的二维码并输入随后显示的代码。
问题
二维码扫描工作正常,Google Authenticator 应用程序可以正确读取它。但是,生成的代码与我在服务器上生成的代码不匹配。
我尝试了什么
我已经尝试了几件事,希望能找到我的问题。
我试过直接插入两个默认密码:
'thiswasmysecretkeyused'
和秘密的base64.b32encode()
编码版本:'ORUGS43XMFZW26LTMVRXEZLUNNSXS5LTMVSA===='
在 Google Authenticator 应用程序中,但这两个生成的代码与服务器不同。我读到密钥的尾随
====
可能会导致它不起作用,所以我也尝试添加一个没有这些的密钥。仍然没有好的结果(他们生成相同的代码)我尝试使用不同的算法生成 TOTP 代码,因为在不太可能发生的情况下,我正在使用的算法(django-otp) is incorrect. The different algorithm I used was taken from this 答案。两种算法在使用时生成相同的代码相同的密钥。
我检查了系统上的时间。我看到操作系统显示
15:03
就像我的智能手机一样。在使用time.time()
和datetime.datetime.now()
转储 python 中的时间后,我看到返回的时间比操作系统时间晚一小时;显示14:03
。我尝试在用于代码生成的时间戳中添加3600
秒,但无济于事。我尝试过其他几种方法,但不太记得它们是什么。
我在 Google Authenticator 中查找了接受密钥的代码,并验证它需要一个 base32 字符串。因此,据我所知,我对密钥的编码是正确的。来自代码(EnterKeyActivity.java,第 78 行):
Verify that the input field contains a valid base32 string
代码
生成密钥;
def generate_shared_key(self):
# create hash etc.
return base64.b32encode(hasher.hexdigest())
正在生成二维码;
key = authenticator.generate_shared_key()
qrcode = pyqrcode.create('otpauth://totp/someurl.nl?secret=' + key)
正在生成 TOTP 代码;
def generate_code(self, drift_steps=0, creation_interval=30, digits=6, t0=0):
code = str(totp(self.generate_shared_key(), creation_interval, timestamp, digits, drift_steps))
return code.zfill(digits)
如果您需要更多代码,例如 django-otp 实际的 totp 生成代码,请告诉我。
错误
没有错误。
预感
我的直觉是,我一定是在密钥生成或将密钥传递给 Google Authenticator 的某个地方出错了。因为即使手动将密钥放入 Google Authenticator 也无法生成正确的代码。 Google Authenticator 是否在保存密钥后对其执行更多操作,例如添加用户?
我还注意到在我使用的另一个算法中,那里的秘密首先被解码;
key = base64.b32decode(secret, True)
我的原始密钥(SHA512 哈希)有误吗?我应该还是不应该使用 base64.b32encode()
对其进行编码?如果我尝试扫描生成的 QR 代码而不对哈希进行编码,Google Authenticator 说它无法将其识别为(有效)密钥。
好吧,经过the code of Google Authenticator的挖掘,我终于找到了我做错了什么。
密钥编码
很清楚:Google验证器是否期望base32
编码字符串作为秘密。因此,无论您是手动输入还是通过二维码输入,当您将密码提供给 Google Authenticator.
base32
编码的字符串
来自 EnterKeyActivity:
/*
* Verify that the input field contains a valid base32 string,
* and meets minimum key requirements.
*/
private boolean validateKeyAndUpdateStatus(boolean submitting) {
//...
}
存储
Google 身份验证器将您提供的密钥按原样存储在数据库中。所以这意味着它将 base32
字符串直接存储在数据库中。
来自 EnterKeyActivity:
private String getEnteredKey() {
String enteredKey = mKeyEntryField.getText().toString();
return enteredKey.replace('1', 'I').replace('0', 'O');
}
protected void onRightButtonPressed() {
//...
if (validateKeyAndUpdateStatus(true)) {
AuthenticatorActivity.saveSecret(this, mAccountName.getText().toString(), getEnteredKey(), null, mode, AccountDb.DEFAULT_HOTP_COUNTER);
exitWizard();
}
//...
}
static boolean saveSecret(Context context, String user, String secret, String originalUser, OtpType type, Integer counter) {
//...
if (secret != null) {
AccountDb accountDb = DependencyInjector.getAccountDb();
accountDb.update(user, secret, originalUser, type, counter);
//...
}
}
检索
当 Google Authenticator 从数据库中检索到秘密时,它会解码 base32
字符串,以便它可以使用真正的秘密。
来自 OtpProvider:
private String computePin(String secret, long otp_state, byte[] challenge) throws OtpSourceException {
//...
try {
Signer signer = AccountDb.getSigningOracle(secret);
//...
}
}
来自 AccountDb:
static Signer getSigningOracle(String secret) {
try {
byte[] keyBytes = decodeKey(secret);
//...
}
}
private static byte[] decodeKey(String secret) throws DecodingException {
return Base32String.decode(secret);
}
错误
我的错误是,在服务器端,我使用 base32
编码的密钥来生成 TOTP 代码,因为我认为 Google Authenticator 也使用它。事后看来,这当然是非常合乎逻辑的,但我找不到太多关于此的信息。希望这会在未来帮助更多人。
TL;DR
确保您传递给 Google 验证器的 secret/key 是一个 base32
编码字符串。确保在服务器端您使用的不是 base32
编码字符串,而是解码字符串。在 Python 中,您可以按如下方式对 secret/key 进行编码和解码:
import base64
base64.b32encode(self.key)
base64.b32decode(self.key)