Google身份验证器代码与服务器生成的代码不匹配

时间:2015-11-30 15:01:54

标签: python django two-factor-authentication google-authenticator one-time-password

背景

<小时/> 我目前正致力于双因素身份验证系统,用户可以使用智能手机进行身份验证。在用户可以使用他们的设备之前,他们需要先验证它。为此,他们需要扫描我给他们的QR码并输入随后显示的代码。

问题

<小时/> 扫描QR码工作正常,Google身份验证器应用程序可以正确读取。但是,生成的代码与我在服务器上生成的代码不匹配。

我尝试了什么

<小时/> 为了找到问题,我尝试了几件事。

  1. 我试过直接插入两个默认密码: 'thiswasmysecretkeyused'和秘密的base64.b32encode()编码版本: Google身份验证器应用中的'ORUGS43XMFZW26LTMVRXEZLUNNSXS5LTMVSA====',但这些代码都生成与服务器不同的代码。

  2. 我看到密钥中的尾随====可能导致它无法正常工作,所以我尝试添加一个没有这些的密码。仍然没有好结果(它们生成相同的代码)

  3. 我尝试过使用不同的算法来生成TOTP代码,因为在不太可能的情况下,我使用的算法(django-otp)不正确。我使用的不同算法来自this回答。当使用相同的密钥时,两种算法都生成相同的代码。

  4. 我检查了系统上的时间。我看到操作系统显示15:03就像我的智能手机一样。在使用time.time()datetime.datetime.now()的python中转储时间后,我看到返回的时间比操作系统时间晚了一个小时;显示14:03。我尝试将3600秒添加到用于代码生成的时间戳中,但无济于事。

  5. 我已经尝试过其他一些事情,但不能回忆起它们的全部内容。

  6. 我查找了接受Google身份验证器中密钥的代码,并验证了它是否需要一个base32字符串。所以我的密钥编码是正确的,据我所知。从代码(EnterKeyActivity.java,第78行):

      

    验证输入字段是否包含有效的base32字符串

  7. 代码

    <小时/> 生成密钥;

    def generate_shared_key(self):
        # create hash etc.
        return base64.b32encode(hasher.hexdigest())
    

    生成QR码;

    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 actual totp生成代码,请告诉我。

    错误

    <小时/> 没有错误。

    预感

    <小时/> 我的预感是,我必须在密钥生成或将密钥传递给Google身份验证器的某个地方出错。因为即使手动将密钥放入Google身份验证器也无法生成正确的代码。 Google身份验证员在保存密钥后是否会使用密钥执行更多操作,例如添加用户?

    我还注意到在其他算法中我使用的秘密首先用;

    进行解码
    key = base64.b32decode(secret, True) 
    

    我的原始密钥(SHA512哈希)是错误的吗?我应该或不应该使用base64.b32encode()对其进行编码吗?如果我尝试扫描生成的QR码而不对哈希进行编码,Google身份验证器表示它不会将其识别为(有效)密钥。

1 个答案:

答案 0 :(得分:8)

好的,经过the code of Google Authenticator挖掘后,我终于找到了我做错的事。

密钥编码

非常清楚:Google身份验证器确实期望将base32编码的字符串作为秘密。因此,无论您是手动输入还是通过QR码输入,都必须确保在将其提供给Google身份验证器时,您的密码是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();
    }
    //...
}

来自AuthenticatorActivity

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身份验证器从数据库中检索到机密时,它会解码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身份验证器也使用了它。事后看来,它当然非常符合逻辑,但我找不到太多关于此的信息。希望这将有助于将来更多的人。

TL; DR

确保您传递给Google身份验证器的密码/密钥是base32编码的字符串。确保在服务器端您没有使用base32编码的字符串,而是使用已解码的字符串。在Python中,您可以按如下方式对密码/密钥进行编码和解码:

import base64

base64.b32encode(self.key)
base64.b32decode(self.key)