概念

TOTP

基于时间的一次性密码算法(Time-Based One-Time Password Algorithm)是一种根据预共享的密钥与当前时间计算一次性密码的算法。

关于 TOTP 的原理介绍参考:rfc6238

一般 TOTP 用在 两步验证(2FA)

MFA

多重要素验证(英语:Multi-factor authentication,缩写为 MFA),又译多因子认证、多因素验证、多因素认证,是一种电脑访问控制的方法,用户要通过两种以上的认证机制之后,才能得到授权,使用电脑资源。

2FA

又作 TFA,是两部验证的缩写(Two-factor authentication)

举例:

  • 使用银行卡时,需要另外输入个人标识符,确认之后才能使用其转账功能。
  • 登录电脑版微信时,用已经登录同一账号的手机版微信扫描特定二维码进行验证。
  • 登录校园网系统时,通过手机短信或学校指定的手机软件进行验证。
  • 登录Steam和Uplay等游戏平台时,使用手机令牌或Google身份验证器进行验证。

TOTP 两步验证流程

用户申请

  1. 用户向服务器请求开启两步验证
  2. 服务生成 Secret 后返回 Secret / URL / QR,并要求用户输入当前的 OTP
  3. 用户使用 TOTP 客户端(比如 Google Authenticator)添加 Secret
  4. 客户端会根据 Secret 和当前时间生成 6 位 OTP
  5. 用户将该 Token 返回给服务器,完成用户和 Secret 的绑定

用户使用

  1. 用户输入用户名密码后,服务端判断是否开启绑定了两步验证
  2. 要求用户输入当前客户端 OTP
  3. 服务端收到用户登录信息以及 OTP,用用户的 Secret 以当前服务器时间生成多个 OTP 进行比对
  4. 比对成功即两步验证通过

TOTP 的基本原理与实现

TOTP authentication explained

生成 Secret 的原理

随机生成 32 位 Base32

public static String generateBase32Secret(int length) {
    StringBuilder sb = new StringBuilder(length);
    Random random = new SecureRandom();
    for (int i = 0; i < length; i++) {
        int val = random.nextInt(32);
        if (val < 26) {
            sb.append((char) ('A' + val));
        } else {
            sb.append((char) ('2' + (val - 26)));
        }
    }
    return sb.toString();
}

一般会以 URL / QR 的方式将这个 Secret 传给用户

URL 格式:otpauth://totp/[用户识别码]?secret=[secret]

例如:otpauth://totp/在下小鲸|[email protected]?secret=NY4A5CPJZ46LXZCP

QR 即二维码将这个 URL 转 QR 即可

验证原理

服务端生成 Secret 给用户的 TOTP 客户端(比如 Google Authenticator),客户端根据当前时间生成六位 OTP(One-Time Password ),提交给服务端验证,服务端根据用户绑定的 Secret 用当前服务器时间去生成 OTP 验证是否合法相等从而决定是否登录成功。所以要求客户端时间和服务端时间需要一致,误差在时间可以在服务端验证算法中设置。

验证客户端传入的 OTP,服务端根据 Sercret 生成 OTP 然后查看是否一致

// 服务端生成 OTP
public static int generateNumber(String base32Secret, long timeMillis, int timeStepSeconds)
    throws GeneralSecurityException {

    // 将 Base32 转 byte 数组
    byte[] key = decodeBase32(base32Secret);

    // 服务器时间处理
    byte[] data = new byte[8];
    long value = timeMillis / 1000 / timeStepSeconds;
    for (int i = 7; value > 0; i--) {
        data[i] = (byte) (value & 0xFF);
        value >>= 8;
    }

    // 指定加密算法
    SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
    // if this is expensive, could put in a thread-local
    Mac mac = Mac.getInstance("HmacSHA1");
    mac.init(signKey);
    byte[] hash = mac.doFinal(data);

    // take the 4 least significant bits from the encrypted string as an offset
    int offset = hash[hash.length - 1] & 0xF;

    // We're using a long because Java hasn't got unsigned int.
    long truncatedHash = 0;
    for (int i = offset; i < offset + 4; ++i) {
        truncatedHash <<= 8;
        // get the 4 bytes at the offset
        truncatedHash |= hash[i] & 0xFF;
    }
    // cut off the top bit
    truncatedHash &= 0x7FFFFFFF;

    // the token is then the last 6 digits in the number
    truncatedHash %= 1000000;
    // 返回 6 位的 token
    return (int) truncatedHash;
}
// 服务端校验 OTP
public static boolean validateCurrentNumber(String base32Secret, int authNumber,
                                            int windowMillis, long timeMillis,
                                            int timeStepSeconds) throws GeneralSecurityException {
    long fromTimeMillis = timeMillis;
    long toTimeMillis = timeMillis;
    if (windowMillis > 0) {
        fromTimeMillis -= windowMillis;
        toTimeMillis += windowMillis;
    }
    long timeStepMillis = timeStepSeconds * 1000;
    // 在允许的误差时间内的所有 OTP 都进行比较
    for (long millis = fromTimeMillis; millis <= toTimeMillis; millis += timeStepMillis) {
        int generatedNumber = generateNumber(base32Secret, millis, timeStepSeconds);
        if (generatedNumber == authNumber) {
            return true;
        }
    }
    return false;
}

Github 项目地址:itWhale233/study-TOTP: TOTP 实验 (github.com)

参考